
Este artigo é sobre como decidimos melhorar um pouco a ferramenta interna SelfTester, usada para verificar a qualidade do analisador PVS-Studio. A melhoria foi simples e parecia útil, mas criou muitos problemas para nós, e mais tarde resultou que seria melhor se não o fizéssemos.
Selftester
Desenvolvemos e promovemos o analisador de código estático PVS-Studio para C, C ++, C # e Java. Para verificar a qualidade do analisador, usamos ferramentas internas chamadas coletivamente de SelfTester. Cada um dos idiomas suportados possui sua própria versão do SelfTester. Isso se deve aos recursos dos testes e é apenas mais conveniente. Portanto, no momento, nossa empresa usa três ferramentas internas do SelfTester para C \ C ++, C # e Java, respectivamente. A seguir, falarei sobre a versão do Windows do SelfTester para projetos do Visual Studio em C \ C ++, chamando-a simplesmente de SelfTester. Este testador foi o primeiro da linha de ferramentas internas, é o mais avançado e mais complexo de todos.
Como o SelfTester funciona? A idéia é simples: pegue um conjunto de projetos de teste (usamos projetos reais de código aberto) e analise-os usando o PVS-Studio. Como resultado, um log de aviso do analisador é gerado para cada projeto. Este log é comparado com o log de
referência para o mesmo projeto. Ao comparar logs, o SelfTester cria um
log de comparação de logs em um formato conveniente para os desenvolvedores perceberem.
Depois de estudar o diário de bordo, o desenvolvedor conclui as mudanças no comportamento do analisador: o número e a natureza dos avisos, a velocidade da operação, há erros internos no analisador, etc. Toda essa informação é muito importante, pois permite entender o desempenho do analisador.
Com base no log de comparação de logs, o desenvolvedor faz alterações no núcleo do analisador (por exemplo, ao criar uma nova regra de diagnóstico), controlando imediatamente o efeito de suas edições. Se o desenvolvedor não tiver mais perguntas sobre a próxima comparação dos logs, ele
fará do log de aviso
atual do projeto uma
referência . Caso contrário, o trabalho continua.
Portanto, a tarefa do SelfTester é trabalhar com um conjunto de projetos de teste (a propósito, já existem mais de 120 deles para C / C ++). Projetos para o pool são selecionados como soluções do Visual Studio. Isso é feito para testar adicionalmente o analisador em diferentes versões do Visual Studio suportadas pelo analisador (do Visual Studio 2010 ao Visual Studio 2019 no momento).
Nota : Separarei ainda mais os conceitos de
solução e
projeto , entendendo o projeto como parte da solução, como é habitual no Visual Studio.
A interface do SelfTester é semelhante a:
À esquerda, há uma lista de soluções, à direita, os resultados dos testes para cada versão do Visual Studio.
As marcas cinza "Não suportado" indicam que a solução não suporta a versão selecionada do Visual Studio ou não foi convertida para esta versão. Algumas soluções no pool têm uma configuração que indica a versão específica do Visual Studio para verificar. Se a versão não for especificada, a solução será atualizada para todas as versões subseqüentes do Visual Studio. Um exemplo dessa solução na captura de tela é "smart_ptr_check.sln" (a verificação foi realizada para todas as versões do Visual Studio).
Uma marca verde “OK” indica que a próxima verificação não revelou diferenças no log de referência. Uma marca vermelha “Diff” indica diferenças. É nesses rótulos que o desenvolvedor deve prestar atenção. Para fazer isso, ele precisa clicar duas vezes no rótulo desejado. A solução selecionada será aberta na versão desejada do Visual Studio e uma janela com um log de aviso também será aberta lá. Os botões de controle abaixo permitem reiniciar a análise das decisões selecionadas ou de todas as decisões, atribuir o registro selecionado (ou todos de uma vez) aos padrões etc.
Os resultados apresentados do trabalho do SelfTester são sempre duplicados no relatório html (log de diferenças).
Além da GUI, o SelfTester também possui modos automatizados para execução durante compilações noturnas. No entanto, o padrão de uso usual é lançamentos repetidos pelo desenvolvedor durante o dia útil. Portanto, uma das características importantes do SelfTester é sua
velocidade .
Por que a velocidade é importante:
- Para executar durante os testes noturnos, o tempo necessário para concluir cada etapa é crítico. Obviamente, quanto mais rápido os testes forem aprovados, melhor. E o tempo médio de operação do SelfTester atualmente excede 2 horas;
- Ao iniciar o SelfTester durante o dia, o desenvolvedor precisa esperar menos pelo resultado, o que aumenta a produtividade do trabalho.
Foi o desejo de acelerar o trabalho do SelfTester que causou as melhorias desta vez.
Multithreading no SelfTester
O SelfTester foi originalmente criado como um aplicativo multiencadeado com a capacidade de verificar várias soluções em paralelo. A única limitação era que você não pode verificar simultaneamente a mesma solução para versões diferentes do Visual Studio, pois muitas soluções precisam ser atualizadas para determinadas versões do Visual Studio antes de verificar. Durante isso, as alterações são feitas diretamente nos
arquivos do projeto
.vcxproj , o que gera erros ao executar em paralelo.
Para tornar o trabalho mais eficiente, o SelfTester usa um agendador de tarefas inteligente, que permite definir um valor estritamente limitado para encadeamentos paralelos e mantê-lo.
O planejador é usado em dois níveis. O primeiro é o nível da
solução , usado para começar a verificar a solução
.sln usando o
utilitário PVS-Studio_Cmd.exe . Dentro do
PVS-Studio_Cmd.exe (no nível de verificação dos
arquivos de código-fonte), o mesmo agendador é usado, mas com um
nível diferente
de configuração
de paralelismo .
O grau de paralelismo é um parâmetro que realmente indica quantos threads paralelos devem ser executados simultaneamente. Para valores de grau de paralelismo no nível de decisão e arquivos, os valores padrão de
quatro e
oito foram selecionados, respectivamente. Portanto, o número de threads paralelos para esta implementação deve ser igual a 32 (quatro soluções testadas simultaneamente e oito arquivos). Essa configuração nos parece ideal para o analisador trabalhar em um processador de oito núcleos.
O desenvolvedor pode definir independentemente outros valores do grau de paralelismo, concentrando-se no desempenho do computador ou nas tarefas atuais. Se ele não definir esse parâmetro, por padrão, o número de processadores lógicos do sistema será selecionado.
Nota : consideraremos ainda que o trabalho é realizado com o grau padrão de valores de paralelismo.
O
planejador LimitedConcurrencyLevelTaskScheduler é herdado de
System.Threading.Tasks.TaskScheduler e refinado para fornecer o nível máximo de paralelismo ao trabalhar na parte superior do
ThreadPool . Hierarquia de herança:
LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler { .... } PausableTaskScheduler: TaskScheduler { .... }
PausableTaskScheduler permite pausar tarefas, e
LimitedConcurrencyLevelTaskScheduler , além disso, fornece controle inteligente da fila de tarefas e agendamento de sua execução, levando em consideração o grau de paralelismo, a quantidade de tarefas agendadas e outros fatores. O planejador é usado ao iniciar as tarefas
System.Threading.Tasks.Task .
Pré-requisitos para melhorias
A implementação do trabalho descrito acima tem uma desvantagem: não é ideal quando se trabalha com soluções de tamanhos diferentes. E o tamanho das soluções no pool de teste é
muito diferente: de 8 KB a 4 GB para o tamanho da pasta com a solução e de um a vários milhares de arquivos de código-fonte em cada um.
O planejador enfileira as decisões simplesmente em ordem, sem nenhum componente intelectual. Deixe-me lembrá-lo de que, por padrão, mais de quatro soluções não podem ser verificadas ao mesmo tempo. Se, no momento, quatro grandes soluções estão sendo verificadas (o número de arquivos em cada uma é superior a oito), presume-se que estamos trabalhando com eficiência, pois usamos o número máximo possível de threads (32).
Mas imagine uma situação bastante comum quando várias pequenas soluções são testadas. Por exemplo, uma solução é grande e contém 50 arquivos (um máximo de oito threads estará envolvido) e as outras três contêm três, quatro e cinco arquivos cada. Nesse caso, usamos apenas 20 threads (8 + 3 + 4 + 5). Temos uma subutilização do tempo do processador e uma diminuição no desempenho geral.
Nota : de fato, o gargalo, como regra, ainda é o subsistema de disco, não o processador.
Melhorias
Uma melhoria que se sugere nesse caso é o ranking da lista de soluções enviadas para verificação. É necessário obter o uso ideal de um determinado número de threads executados simultaneamente (32) enviando projetos com o número "correto" de arquivos para verificação.
Vejamos nosso exemplo novamente, quando quatro soluções são testadas com o seguinte número de arquivos em cada uma: 50, 3, 4 e 5. Uma tarefa que verifica uma solução de
três arquivos provavelmente funcionará em breve. E, em vez disso, seria ideal adicionar uma solução na qual haja oito ou mais arquivos (para usar no máximo oito fluxos disponíveis para esta solução). No total, usaremos já 25 threads (8 +
8 + 4 + 5). Nada mal. No entanto, sete threads ainda não foram utilizados. E aqui surge a idéia de outro refinamento, relacionada à remoção da restrição em quatro threads para verificação de soluções. De fato, no exemplo acima, você pode adicionar não uma, mas várias soluções, usando o máximo possível todos os 32 threads. Vamos imaginar que temos mais duas soluções, três e quatro arquivos cada. A adição dessas tarefas fechará completamente a “lacuna” nos encadeamentos não utilizados e haverá 32 (8 + 8 + 4 + 5 +
3 +
4 ).
Eu acho que a ideia é clara. De fato, a implementação dessas melhorias também não exigiu muito esforço. Tudo foi feito em um dia.
Era necessário refinar a classe de tarefa: herança de
System.Threading.Tasks.Task e adicionar o campo "weight". Para definir o peso da solução, um algoritmo simples é usado: se o número de arquivos na solução for menor que oito, o peso será definido igual a esse valor (por exemplo, 5), se o número de arquivos for maior ou igual a oito, o peso será escolhido igual a oito.
Também era necessário refinar o agendador: ensiná-lo a escolher soluções com o peso certo para atingir um valor máximo de 32 threads. Também era necessário permitir a alocação de mais de quatro threads para verificação simultânea de soluções.
Por fim, foi necessária uma etapa preliminar para analisar todas as soluções de pool (avaliação usando a API do MSBuild) para calcular e definir os pesos da solução (obter o número de arquivos com código fonte).
Resultado
Acho que, após uma introdução tão longa, você já adivinhou que o resultado era zero.
É bom que as melhorias sejam simples e rápidas.
Bem, agora, de fato, começa a parte do artigo sobre "criou muitos problemas para nós", e é tudo.
Efeitos colaterais
Portanto, um resultado negativo também é um resultado. Verificou-se que o número de soluções grandes no pool excede
significativamente o número de pequenas (menos de oito arquivos). Nessas condições, as melhorias realizadas não têm um efeito perceptível, pois são praticamente invisíveis: sua verificação leva um tempo microscópico em comparação aos grandes projetos.
No entanto, foi decidido deixar a revisão como "não interferindo" e potencialmente útil. Além disso, o conjunto de soluções de teste é constantemente reabastecido; portanto, no futuro, talvez, a situação mude.
E então ...
Um dos desenvolvedores reclamou da "queda" do SelfTester. Bem, acontece. Para evitar que esse erro seja perdido, um incidente interno (ticket) foi iniciado com o nome “Exceção ao trabalhar com o SelfTester”. O erro ocorreu durante a avaliação do projeto. É verdade que uma abundância de janelas testemunhou o problema também no manipulador de erros. Mas isso foi rapidamente eliminado e, na semana seguinte, nada quebrou. De repente, outro usuário reclamou do SelfTester. E, novamente, ao erro da avaliação do projeto:
Desta vez, a pilha continha informações mais úteis - um erro no formato xml. Provavelmente, ao processar o arquivo de projeto
Proto_IRC.vcxproj (sua representação em xml), algo aconteceu com o próprio arquivo, portanto, o
XmlTextReader não pôde processá-lo.
A presença de dois erros em um período de tempo bastante curto nos levou a examinar mais de perto o problema. Além disso, como eu disse acima, o SelfTester é usado ativamente pelos desenvolvedores.
Para começar, foi feita uma análise do último local do outono. Infelizmente, nada suspeito pode ser identificado. Por precaução, eles pediram aos desenvolvedores (usuários do SelfTester) que estivessem alertas e relatassem possíveis erros.
Um ponto importante: o código no qual o erro ocorreu foi reutilizado no SelfTester. Inicialmente, é usado para avaliar projetos no próprio analisador (
PVS-Studio_Cmd.exe ). É por isso que a atenção ao problema aumentou. No entanto, nenhuma queda semelhante ocorreu no analisador.
Enquanto isso, um ticket sobre problemas com o SelfTester foi reabastecido com novos erros:
E novamente
XmlException . Obviamente, em algum lugar há threads concorrentes trabalhando com arquivos de projeto de leitura e gravação. O SelfTester trabalha com projetos nos seguintes casos:
- Avaliação de projetos durante o cálculo preliminar de pesos de decisão: uma nova etapa que inicialmente despertou suspeitas;
- Atualizando projetos para as versões necessárias do Visual Studio: executadas imediatamente antes da verificação (os projetos não se cruzam de forma alguma) e não devem afetar o trabalho;
- Avaliação de projetos durante a verificação: mecanismo seguro de thread depurado, que foi reutilizado no PVS-Studio_Cmd.exe ;
- Recuperando arquivos de projeto (substituindo arquivos .vcxproj modificados pelos arquivos de referência originais) ao sair do SelfTester, pois os arquivos de projeto podem ser atualizados para as versões necessárias do Visual Studio no processo: a etapa final, que também não afeta outros mecanismos.
Suspeita-se do novo código adicionado para otimização (cálculo de pesos). Porém, o estudo desse código mostrou que, se o usuário iniciava a análise imediatamente após o início do SelfTester, o testador sempre aguardava corretamente o final da avaliação preliminar. Este lugar parecia seguro.
Mais uma vez, não conseguimos identificar a fonte do problema.
Dor
Durante o mês seguinte, o SelfTester continuou a cair de tempos em tempos. O ticket foi reabastecido com dados, mas não estava claro o que fazer com esses dados. A maioria das falhas ocorreu com o mesmo
XmlException . Ocasionalmente, havia algo mais, mas no mesmo código reutilizado do
PVS-Studio_Cmd.exe .
Por tradição, não são impostos requisitos tão altos às ferramentas internas; portanto, o trabalho com erros do SelfTester foi realizado de forma residual. De tempos em tempos, pessoas diferentes se conectavam (durante todo o período do incidente, seis pessoas trabalhavam no problema, incluindo dois estagiários). No entanto, a tarefa teve que ser distraída.
Nosso primeiro erro. De fato, aqui já era possível resolver o problema de uma vez por todas. Como Ficou claro que o erro foi causado por uma nova otimização. Afinal, antes disso, tudo funcionava bem e o código reutilizado obviamente não podia ser tão ruim. Além disso, essa otimização não trouxe nenhum benefício. Então, o que tinha que ser feito?
Remova essa otimização . Como você sabe, isso não foi feito. Continuamos trabalhando em um problema que nós mesmos criamos. A busca continuou pela resposta à pergunta: "COMO ???" Como cai? No entanto, parece estar escrito corretamente.
Nosso segundo erro. Outras pessoas estavam
conectadas à solução do problema. Um erro muito, muito grande. Infelizmente, isso não apenas não resolveu o problema, mas também foram gastos recursos adicionais. Sim, novas pessoas trouxeram novas idéias, mas, para sua implementação, levou (absolutamente desperdiçado) muito tempo de trabalho. Em um certo estágio, os programas de teste foram escritos (pelos mesmos estagiários) que simulam a avaliação do mesmo projeto em diferentes threads com modificação paralela do projeto em outro thread. Não ajudou. Além do que já sabíamos antes, a API do MSBuild é segura para threads por dentro, eles não descobriram nada de novo. E no SelfTester, um mini-despejo foi adicionado quando um
XmlException foi lançado. Então tudo isso alguém estremeceu, horror. Discussões foram realizadas, muitas outras coisas desnecessárias foram feitas.
Finalmente, nosso terceiro erro . Você sabe quanto tempo se passou desde que o problema com o SelfTester surgiu e até que ele foi resolvido? Embora não, conte-se. O incidente foi criado em 17/09/2018 e encerrado em 20/02/2019, e existem mais de 40 (quarenta!) Mensagens lá. Gente, isso é muito tempo! Nós
nos permitimos fazer TI
por cinco meses. Ao mesmo tempo (em paralelo), estávamos empenhados em oferecer suporte ao Visual Studio 2019, adicionando a linguagem Java, começando a implementar o padrão MISRA C / C ++, aprimorando o analisador C #, participando ativamente de conferências, escrevendo vários artigos, etc. E todos esses trabalhos não receberam o tempo dos desenvolvedores devido ao erro estúpido do SelfTester.
Cidadãos, aprendam com nossos erros e nunca fazem isso. E nós não vamos.
Eu tenho tudo
Claro, isso é uma piada, e vou lhe dizer qual foi o problema com o SelfTester :)
Bingo!
Felizmente, entre nós havia uma pessoa com a consciência menos nublada (meu colega Sergey Vasiliev), que apenas olhou o problema de um ângulo completamente diferente (e também teve um pouco de sorte). E se o SelfTester estiver realmente bom e os projetos quebrarem algo do lado de fora? Paralelamente ao SelfTester, geralmente nada foi iniciado; em alguns casos, controlamos rigorosamente o tempo de execução. Nesse caso, esse "algo" só poderia ser o próprio SelfTester, mas outra instância.
Ao sair do SelfTester, o fluxo de restauração de arquivos de projeto dos padrões continua funcionando por algum tempo. Neste ponto, você pode reiniciar o testador. A proteção contra a execução de várias instâncias do SelfTester ao mesmo tempo foi adicionada
posteriormente e agora se parece com isso:
Mas então ela se foi.
Incrivelmente, por quase meio ano de tormento, ninguém prestou atenção nisso. Restaurar projetos a partir de padrões é um procedimento em segundo plano rápido o suficiente, mas, infelizmente, não é rápido o suficiente para não interferir na reinicialização do SelfTester. E o que acontece na inicialização? É isso mesmo, calculando pesos de decisão. Um processo substitui arquivos
.vcxproj , enquanto outro tenta lê-los. Olá,
XmlException .
Sergey descobriu tudo isso quando adicionou ao testador a capacidade de mudar para o modo de trabalhar com outro conjunto de logs padrão. A necessidade disso surgiu após a adição do conjunto de regras MISRA ao analisador. Você pode alternar diretamente na interface, enquanto o usuário vê a janela:
Após o que o SelfTester é
reiniciado . Bem, antes, aparentemente, os usuários de alguma maneira imitaram o problema, iniciando o testador novamente.
Discussão e conclusões
Obviamente, excluímos ou desativamos a otimização criada anteriormente. Além disso, era muito mais fácil do que fazer algum tipo de sincronização entre o restante do testador. E tudo começou a funcionar muito bem, como antes. E como uma medida adicional, a proteção descrita acima contra o lançamento simultâneo do testador foi adicionada.
Eu já escrevi acima sobre nossos principais erros durante a busca do problema, de modo que a auto-flagelação é suficiente. Também somos pessoas e, portanto, estamos enganados. É importante aprender com seus erros e tirar conclusões. As conclusões aqui são bastante simples:
- É necessário rastrear e avaliar o crescimento da complexidade da tarefa;
- Pare no tempo;
- Tente analisar o problema de maneira mais ampla, pois com o tempo a visão fica "borrada" e o ângulo de visão é reduzido;
- Não tenha medo de excluir código antigo ou desnecessário.
Agora, com certeza - é isso. Obrigado pela leitura. Para todo o código sem esperança!

Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Sergey Khrenov.
O melhor é o inimigo do bem .