
Cet article explique comment, une fois que nous avons décidé d'améliorer légèrement l'outil interne SelfTester, utilisé pour vérifier la qualité de l'analyseur PVS-Studio. L'amélioration était simple et semblait utile, mais elle a créé beaucoup de problèmes pour nous, et plus tard il s'est avéré que ce serait mieux si nous ne le faisions pas.
Selftester
Nous développons et promouvons l'analyseur de code statique PVS-Studio pour C, C ++, C # et Java. Pour vérifier la qualité de l'analyseur, nous utilisons des outils internes appelés collectivement SelfTester. Chacune des langues prises en charge a sa propre version de SelfTester. Cela est dû aux fonctionnalités des tests, et c'est juste plus pratique. Ainsi, pour le moment, notre entreprise utilise trois outils SelfTester internes pour C \ C ++, C # et Java, respectivement. Ensuite, je vais 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 dans la gamme de ces outils internes, il 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 avec du code open source) et les analyser à l'aide de PVS-Studio. Par conséquent, un journal d'avertissement de l'analyseur est généré pour chaque projet. Ce journal est comparé au journal de
référence pour le même projet. Lors de la comparaison des journaux, SelfTester crée un
journal de comparaison des journaux sous une forme que les développeurs peuvent percevoir.
Après avoir étudié le journal de bord, le développeur tire une conclusion sur les changements dans le comportement de l'analyseur: le nombre et la nature des avertissements, la vitesse de fonctionnement, il y a des erreurs internes de l'analyseur, etc. Toutes ces informations sont très importantes, elles vous permettent de comprendre dans quelle mesure l'analyseur fait son travail.
Sur la base du journal de comparaison des journaux, le développeur apporte des modifications au cœur de l'analyseur (par exemple, lors de la création d'une nouvelle règle de diagnostic), contrôlant immédiatement l'effet de ses modifications. Si le développeur n'a plus de questions sur la prochaine comparaison des journaux, il
fait du journal d'avertissement
actuel du projet une
référence . Sinon, le travail continue.
Ainsi, la tâche de SelfTester est de travailler avec un pool de projets de test (au fait, il y en a déjà plus de 120 pour C / C ++). Les projets pour le pool sont sélectionnés en tant que solutions Visual Studio. Ceci est effectué afin de tester en plus l'analyseur sur différentes versions de Visual Studio que l'analyseur prend en charge (de Visual Studio 2010 à Visual Studio 2019 pour le moment).
Remarque : Je séparerai davantage les concepts de
solution et de
projet , en comprenant le projet comme faisant partie de la solution, comme c'est la coutume dans Visual Studio.
L'interface SelfTester ressemble à:
À gauche, une liste de solutions, à droite, les résultats des tests pour chaque version de Visual Studio.
Les repères gris «Non pris en charge» indiquent que la solution ne prend pas en charge la version sélectionnée de Visual Studio ou n'a pas été convertie pour cette version. Certaines solutions du pool ont un paramètre qui indique la version spécifique de Visual Studio à vérifier. Si la version n'est pas spécifiée, la solution sera mise à jour vers toutes les versions ultérieures de Visual Studio. Un exemple d'une telle solution dans la capture d'écran est «smart_ptr_check.sln» (la vérification a été effectuée pour toutes les versions de Visual Studio).
Une marque verte «OK» indique que la vérification suivante n'a révélé aucune différence avec le journal de référence. Un repère «Diff» rouge indique des différences. C'est sur de telles étiquettes que le développeur doit faire attention. Pour ce faire, il doit double-cliquer sur l'étiquette souhaitée. La solution sélectionnée sera ouverte dans la version souhaitée de Visual Studio et une fenêtre avec un journal d'avertissement y sera également ouverte. Les boutons de contrôle ci-dessous vous permettent de redémarrer l'analyse des décisions sélectionnées ou de toutes les décisions, d'affecter le journal sélectionné (ou tout d'un coup) aux journaux standard, etc.
Les résultats présentés du travail SelfTester sont toujours dupliqués dans le rapport html (journal des différences).
En plus de l'interface graphique, SelfTester dispose également de modes automatisés pour l'exécution pendant les générations nocturnes. Cependant, le schéma d'utilisation habituel est des lancements répétés par le développeur pendant la journée de travail. Par conséquent, l'une des caractéristiques importantes de SelfTester est sa
vitesse .
Pourquoi la vitesse est importante:
- Pour s'exécuter pendant les tests de nuit, le temps nécessaire pour terminer chaque étape est critique. De toute évidence, plus les tests réussissent rapidement, mieux c'est. Et le temps de fonctionnement moyen de SelfTester dépasse actuellement 2 heures;
- Lors du lancement de SelfTester pendant la journée, le développeur doit attendre moins le résultat, ce qui augmente la productivité du travail.
C'est le désir d'accélérer le travail de SelfTester qui a provoqué les améliorations cette fois.
Multithreading dans SelfTester
SelfTester a été initialement créé comme une application multi-thread avec la possibilité de vérifier plusieurs solutions en parallèle. La seule limitation était que vous ne pouvez 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 vérifier. Pendant cela, des modifications sont apportées directement aux
fichiers de projet
.vcxproj , ce qui entraîne des erreurs lors de l'exécution en parallèle.
Pour rendre le travail plus efficace, SelfTester utilise un planificateur de tâches intelligent, qui vous permet de définir une valeur strictement limitée pour les threads parallèles et de la maintenir.
L'ordonnanceur est utilisé à deux niveaux. Le premier est le niveau de
solution , qui est utilisé pour commencer à vérifier la solution
.sln à l'aide de l'
utilitaire PVS-Studio_Cmd.exe . Dans
PVS-Studio_Cmd.exe (au niveau de la vérification des
fichiers de code source), le même planificateur est utilisé, mais avec un paramètre
de parallélisme différent.
Le degré de parallélisme est un paramètre qui indique réellement combien de threads parallèles doivent s'exécuter simultanément. Pour les valeurs de degré de parallélisme au niveau de la décision et des fichiers, les valeurs par défaut de
quatre et
huit ont été sélectionnées, respectivement. Ainsi, le nombre de threads parallèles pour cette implémentation doit être égal à 32 (quatre solutions testées simultanément et huit fichiers). Ce paramètre nous semble optimal pour que l'analyseur fonctionne sur un processeur à huit cœurs.
Le développeur peut définir indépendamment d'autres valeurs du degré de parallélisme, en se concentrant sur les performances de son ordinateur ou des tâches en cours. S'il ne définit pas ce paramètre, par défaut, le nombre de processeurs logiques du système sera sélectionné.
Remarque : nous considérerons en outre que le travail est effectué avec les valeurs par défaut du degré de parallélisme.
Le
planificateur LimitedConcurrencyLevelTaskScheduler est hérité de
System.Threading.Tasks.TaskScheduler et affiné pour fournir le niveau maximal de parallélisme lorsque vous travaillez sur
ThreadPool . Hiérarchie d'héritage:
LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler { .... } PausableTaskScheduler: TaskScheduler { .... }
PausableTaskScheduler vous permet de suspendre l'exécution des tâches, et
LimitedConcurrencyLevelTaskScheduler , en outre, fournit un contrôle intelligent de la file d'attente des tâches et la planification de leur exécution, en tenant compte du degré de parallélisme, de la quantité de tâches planifiées et d'autres facteurs. Le planificateur est utilisé lors du démarrage des tâches
System.Threading.Tasks.Task .
Conditions préalables aux améliorations
La mise en œuvre des travaux décrits ci-dessus présente un inconvénient: elle n'est pas optimale lorsque l'on travaille avec des solutions de tailles différentes. Et la taille des solutions dans le pool de tests est
très différente: de 8 Ko à 4 Go pour la taille du dossier avec la solution, et d'un à plusieurs milliers de fichiers de code source dans chacun.
L'ordonnanceur met les décisions en file d'attente simplement dans l'ordre, sans aucune composante intellectuelle. Permettez-moi de vous rappeler que par défaut, plus de quatre solutions ne peuvent pas être vérifiées en même temps. Si, à l'heure actuelle, quatre grandes solutions sont vérifiées (le nombre de fichiers dans chacune est supérieur à huit), il est supposé que nous travaillons efficacement, car nous utilisons le nombre maximal possible de threads (32).
Mais imaginez une situation assez courante lorsque plusieurs petites solutions sont testées. Par exemple, une solution est volumineuse et contient 50 fichiers (un maximum de huit threads seront impliqués), et les trois autres contiennent trois, quatre et cinq fichiers chacun. Dans ce cas, nous n'utilisons que 20 threads (8 + 3 + 4 + 5). Nous obtenons une sous-utilisation du temps processeur et une diminution des performances globales.
Remarque : en fait, le goulot d'étranglement, en règle générale, est toujours le sous-système de disque, pas le processeur.
Améliorations
Une amélioration qui se propose dans ce cas est le classement de la liste des solutions soumises à vérification. Il est nécessaire de parvenir à une utilisation optimale d'un nombre donné de threads exécutés simultanément (32) en soumettant des projets avec le nombre «correct» de fichiers pour vérification.
Regardons à nouveau notre exemple, lorsque quatre solutions sont testées avec le nombre de fichiers suivant dans chacune: 50, 3, 4 et 5. Une tâche qui vérifie une solution de
trois fichiers fonctionnera probablement bientôt. Et au lieu de cela, il serait optimal d'ajouter une solution dans laquelle il y a huit fichiers ou plus (afin d'utiliser un maximum de huit flux disponibles pour cette solution). Ensuite, au total, nous utiliserons déjà 25 threads (8 +
8 + 4 + 5). Pas mal. Cependant, sept threads étaient encore inutilisés. Et ici surgit l'idée d'un autre raffinement, lié à la suppression de la restriction sur quatre threads pour la vérification des solutions. En effet, dans l'exemple ci-dessus, vous pouvez ajouter non pas une, mais plusieurs solutions, en utilisant autant que possible les 32 threads. Imaginons que nous ayons deux autres solutions, trois et quatre fichiers chacune. L'ajout de ces tâches comblera complètement «l'écart» dans les threads inutilisés, et il y en aura 32 (8 + 8 + 4 + 5 +
3 +
4 ).
Je pense 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.
Il a fallu affiner la classe de tâches: héritage de
System.Threading.Tasks.Task et ajouter le champ "weight". Pour définir le poids de la solution, un algorithme simple est utilisé: si le nombre de fichiers dans la solution est inférieur à huit, alors le poids est défini égal à cette valeur (par exemple, 5), si le nombre de fichiers est supérieur ou égal à huit, alors le poids est choisi égal à huit.
Il a également fallu affiner l'ordonnanceur: lui apprendre à choisir des solutions avec le bon poids pour atteindre une valeur maximale de 32 threads. Il était également nécessaire de permettre l'allocation de plus de quatre threads pour la vérification simultanée des solutions.
Enfin, il a fallu une étape préliminaire pour analyser toutes les solutions de pool (évaluation à l'aide de l'API MSBuild) pour calculer et définir les pondérations de la solution (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 le résultat était nul.
Heureusement que les améliorations ont été simples et rapides.
Eh bien, maintenant, en fait, la partie de l'article sur «a créé beaucoup de problèmes pour nous» commence, et c'est tout.
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
considérablement le nombre de petites (moins de huit fichiers). Dans ces conditions, les améliorations apportées n'ont pas d'effet notable, car elles sont pratiquement invisibles: leur vérification prend un temps microscopique par rapport aux grands projets.
Néanmoins, il a été décidé de laisser la révision comme "sans interférence" 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 de la "chute" de SelfTester. Eh bien, ça arrive. Pour éviter que cette erreur ne soit perdue, un incident interne (ticket) a été lancé avec le nom "Exception lors de l'utilisation de SelfTester". L'erreur s'est produite lors de l'évaluation du projet. Certes, une telle abondance de fenêtres témoigne également du problème dans le gestionnaire d'erreurs. Mais cela a été rapidement éliminé et au cours de la semaine suivante, rien ne s'est cassé. Soudain, un autre utilisateur s'est plaint de SelfTester. Et encore une fois à l'erreur de l'évaluation du projet:
La pile contenait cette fois des informations plus utiles - une erreur au format xml. Probablement, lors du traitement du fichier de projet
Proto_IRC.vcxproj (sa représentation xml), quelque chose est arrivé au fichier lui-même, donc
XmlTextReader n'a pas pu le traiter.
La présence de deux erreurs dans 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, une analyse a été faite du dernier lieu de l'automne. Malheureusement, rien de suspect n'a pu être identifié. Au cas où, ils ont demandé aux développeurs (utilisateurs SelfTester) d'être en alerte et de signaler d'éventuelles erreurs.
Un point important: le code sur lequel l'erreur s'est produite a été réutilisé dans SelfTester. Initialement, il est 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 s'est accrue. Cependant, aucune baisse similaire ne s'est produite dans l'analyseur.
Pendant ce temps, un ticket sur les problèmes avec SelfTester a été réapprovisionné avec de nouvelles erreurs:
Et encore
une fois
XmlException . Évidemment, quelque part, il existe des threads concurrents qui travaillent avec des fichiers de projet en lecture et en écriture. SelfTester fonctionne avec des projets dans les cas suivants:
- Évaluation des projets lors du calcul préliminaire des poids de décision: une nouvelle étape qui a d'abord suscité des soupçons;
- Mise à niveau des projets vers les versions nécessaires de Visual Studio: effectuée immédiatement avant la vérification (les projets ne se croisent en aucune façon) et ne doit pas affecter le travail;
- Évaluation des projets lors de la vérification: débogage du mécanisme thread-safe, qui a été réutilisé à partir de PVS-Studio_Cmd.exe ;
- Récupération des fichiers de projet (remplacement des fichiers .vcxproj modifiés par les fichiers de référence d'origine) à la sortie de SelfTester, car les fichiers de projet peuvent être mis à jour vers les versions nécessaires de Visual Studio dans le processus: la dernière étape, qui n'affecte pas non plus les autres mécanismes.
Les soupçons sont tombés sur le nouveau code ajouté pour l'optimisation (calcul des poids). Mais l'étude de ce code a montré que si l'utilisateur commençait l'analyse immédiatement après le démarrage de SelfTester, le testeur attendait toujours correctement la fin de l'évaluation préliminaire. Cet endroit avait l'air sûr.
Encore une fois, nous n'avons pas pu identifier la source du problème.
La douleur
Au cours du mois suivant, SelfTester a continué de chuter de temps en temps. Le ticket a été réapprovisionné en données, mais il n'était pas clair que faire de ces données. La plupart des plantages étaient tous 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 .
Par tradition, les outils internes ne sont pas soumis à des exigences si élevées, de sorte que le travail avec les erreurs SelfTester a été effectué de manière résiduelle. De temps en temps, différentes personnes se sont connectées (pendant toute la durée de l'incident, six personnes ont travaillé sur le problème, dont deux stagiaires stagiaires). Néanmoins, la tâche devait être distraite.
Notre première erreur. En fait, il était déjà possible de résoudre le problème une fois pour toutes. Comment? Il était clair que l'erreur était due à une nouvelle optimisation. Après tout, avant cela, tout fonctionnait bien et le code réutilisé ne pouvait évidemment pas être si mauvais. De plus, cette optimisation n'a apporté aucun avantage. Alors, que fallait-il faire?
Supprimez cette optimisation . Comme vous le savez, cela n'a pas été fait. Nous avons continué à travailler sur un problème que nous avions nous-mêmes créé. La recherche s'est poursuivie pour trouver la réponse à la question: "COMMENT ???" Comment ça tombe? Pourtant, il semble être écrit correctement.
Notre deuxième erreur. D'autres personnes ont été
connectées à la solution du problème. Une très, très grosse erreur. Malheureusement, non seulement cela n'a pas résolu le problème, mais des ressources supplémentaires ont été dépensées. Oui, de nouvelles personnes ont apporté de nouvelles idées, mais pour leur mise en œuvre, cela a pris (absolument gaspillé) beaucoup de temps de travail. À un certain stade, des programmes de test ont été écrits (par les mêmes stagiaires) qui émulent l'évaluation du même projet dans différents threads avec une modification parallèle du projet dans un autre thread. Ça n'a pas aidé. En plus de ce que nous savions déjà, l'API MSBuild est sécurisée pour les threads, ils n'ont rien découvert de nouveau. Et dans SelfTester, un mini-vidage a été ajouté lorsqu'une
XmlException a été levée. Puis tout cela, quelqu'un de débraillé, d'horreur. Des discussions ont eu lieu, beaucoup d'autres choses inutiles ont été faites.
Enfin, notre troisième erreur . Savez-vous combien de temps s'est écoulé depuis que le problème avec SelfTester est survenu jusqu'à ce qu'il soit résolu? Bien que non, comptez-vous. L'incident a été créé le 17/09/2018 et fermé le 20/02/2019, et il y a plus de 40 (quarante!) Messages là-bas. Les gars, c'est beaucoup de temps! Nous
nous sommes
permis de le faire
pendant cinq mois. Dans le même temps (en parallèle), nous nous sommes engagés à prendre en charge Visual Studio 2019, à ajouter le langage Java, à commencer à implémenter la norme MISRA C / C ++, à améliorer l'analyseur C #, à participer activement à des conférences, à écrire un tas d'articles, etc. Et tous ces travaux n'ont pas reçu le temps des développeurs en raison de la stupide erreur SelfTester.
Citoyens, apprenez de nos erreurs et ne faites jamais cela. Et nous ne le ferons pas.
J'ai tout.
Bien sûr, c'est une blague, et je vais vous dire quel était le problème avec SelfTester :)
Bingo!
Heureusement, parmi nous, il y avait une personne avec la conscience la moins trouble (mon collègue Sergey Vasiliev), qui vient de regarder le problème sous un angle complètement différent (et aussi il a eu un peu de chance). Et si l'intérieur de SelfTester allait vraiment bien et que les projets cassaient quelque chose de l'extérieur? En parallèle avec SelfTester, généralement rien n'a commencé, dans certains cas, nous contrôlions strictement le temps d'exécution. Dans ce cas, ce «quelque chose» ne peut être que SelfTester lui-même, mais une autre instance de celui-ci.
En quittant SelfTester, le flux de restauration des fichiers de projet à partir des normes continue de fonctionner pendant un certain temps. À ce stade, vous pouvez redémarrer le testeur. La protection contre l'exécution de plusieurs instances de SelfTester en même temps a été ajoutée
plus tard et ressemble maintenant à ceci:
Mais alors elle était partie.
Incroyablement, pendant près de six mois de tourments, personne n'y a prêté attention. La restauration de projets à partir des normes est une procédure d'arrière-plan assez rapide, mais malheureusement pas assez rapide pour ne pas interférer avec le redémarrage de SelfTester. Et que se passe-t-il au démarrage? C'est vrai, calculer les poids de décision. Un processus écrase les fichiers
.vcxproj , tandis qu'un autre essaie de les lire. Salut,
XmlException .
Sergey a découvert tout cela en ajoutant au testeur la possibilité de passer au mode de travail avec un autre ensemble de journaux standard. Le besoin s'est fait sentir après l'ajout de l'ensemble de règles MISRA à l'analyseur. Vous pouvez basculer directement dans l'interface, pendant que l'utilisateur voit la fenêtre:
Après quoi SelfTester
redémarre . Eh bien, plus tôt, apparemment, les utilisateurs ont eux-mêmes émulé le problème en redémarrant le testeur.
Débriefing et conclusions
Bien sûr, nous avons supprimé, ou plutôt désactivé, l'optimisation précédemment créée. De plus, c'était beaucoup plus facile que de faire une sorte de synchronisation entre le reste du testeur lui-même. Et tout a commencé à bien fonctionner, comme avant. Et comme mesure supplémentaire, la protection décrite ci-dessus contre le lancement simultané du testeur a été ajoutée.
J'ai déjà écrit ci-dessus nos principales erreurs lors de la recherche du problème, donc l'auto-flagellation suffit. Nous sommes aussi des gens et nous nous trompons donc. Il est important d'apprendre de vos erreurs et de tirer des conclusions. Les conclusions ici sont assez simples:
- Il est nécessaire de suivre et d'évaluer la croissance de la complexité des tâches;
- Arrêtez-vous à temps;
- Essayez de regarder le problème plus largement, car au fil du temps la vue est «floue» et l'angle de vue se rétrécit;
- N'ayez pas peur de supprimer du code ancien ou inutile.
Maintenant, c'est sûr - c'est tout. Merci d'avoir lu. Pour tout code désespéré!

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Sergey Khrenov.
Le meilleur est l'ennemi du bien .