
Este artigo é a história de como decidimos melhorar nossa ferramenta interna de Autoteste que aplicamos para testar a qualidade do analisador PVS-Studio. A melhoria foi simples e parecia útil, mas nos meteu em alguns problemas. Mais tarde, descobrimos que é melhor desistirmos da ideia.
Selftester
Desenvolvemos e promovemos o analisador de código estático PVS-Studio para C, C ++, C # e Java. Para testar a qualidade do nosso analisador, usamos ferramentas internas, chamadas genericamente de SelfTester. Criamos uma versão separada do SelfTester para cada idioma suportado. Isso se deve a especificidades dos testes e é apenas mais conveniente. Portanto, no momento, temos três ferramentas internas do SelfTester em nossa empresa para C \ C ++, C # e Java, respectivamente. Além disso, vou falar sobre a versão Windows do SelfTester para projetos do Visual Studio em C \ C ++, chamando-a simplesmente SelfTester. Este testador foi o primeiro da linha de ferramentas internas semelhantes, é o mais avançado e complexo de todos.
Como o SelfTester funciona? A idéia é simples: pegue um conjunto de projetos de teste (estamos usando projetos reais de código aberto) e analise-os usando o PVS-Studio. Como resultado, um log do analisador é gerado para cada projeto. Este log é comparado com o log de
referência do mesmo projeto. Ao comparar logs, o SelfTester cria um
resumo dos logs comparando de uma maneira conveniente para o desenvolvedor.
Depois de estudar o resumo, um desenvolvedor conclui sobre alterações no comportamento do analisador de acordo com o número e tipo de avisos, velocidade de trabalho, erros internos do analisador, etc. Toda essa informação é muito importante: permite que você esteja ciente de como o analisador lida com seu trabalho.
Com base no resumo da comparação de logs, um desenvolvedor introduz alterações no núcleo do analisador (por exemplo, ao criar uma nova regra de diagnóstico) e controla imediatamente o resultado de suas edições. Se um desenvolvedor não tiver mais problemas na comparação regular de logs, ele fará uma
referência atual do log de avisos para um projeto. Caso contrário, o trabalho continua.
Portanto, a tarefa do SelfTester é trabalhar com um conjunto de projetos de teste (a propósito, existem mais de 120 deles para C / C ++). Projetos para o pool são selecionados na forma de soluções do Visual Studio. Isso é feito para verificar adicionalmente o trabalho do analisador em várias versões do Visual Studio, que suportam o analisador (neste momento, do Visual Studio 2010 ao Visual Studio 2019).
Nota: além disso, separarei os conceitos
solução e
projeto , considerando um projeto como parte de uma solução.
A interface do SelfTester tem a seguinte aparência:
À esquerda, há uma lista de soluções, à direita - resultados de uma verificação para cada versão do Visual Studio.
Rótulos em cinza "Não suportado" indicam que uma solução não suporta uma versão escolhida do Visual Studio ou não foi convertida para esta versão. Algumas soluções têm uma configuração em um pool, que indica uma versão específica do Visual Studio para uma verificação. Se uma versão não for especificada, uma solução será atualizada para todas as versões subseqüentes do Visual Studio. Um exemplo dessa solução está na captura de tela - "smart_ptr_check.sln" (é feita uma verificação para todas as versões do Visual Studio).
Um rótulo verde "OK" indica que uma verificação regular não detectou diferenças com o log de referência. Um rótulo vermelho "Dif." Indica sobre diferenças. Esses rótulos devem receber atenção especial. Depois de clicar duas vezes no rótulo necessário, a solução escolhida será aberta em uma versão relacionada do Visual Studio. Uma janela com um log de avisos também será aberta lá. Os botões de controle na parte inferior permitem executar novamente a análise das soluções selecionadas ou de todas as soluções, fazer a referência do log escolhido (ou de uma só vez), etc.
Os resultados do SelfTester são sempre duplicados no relatório html (relatório diffs)
Além da GUI, o SelfTester também possui modos automatizados para execuções noturnas. No entanto, o padrão de uso usual que o desenvolvedor repetido executa por um desenvolvedor durante o dia de trabalho. Portanto, uma das características mais importantes do SelfTester é a velocidade do trabalho.
Por que a velocidade é importante:
- O desempenho de cada etapa é bastante crucial em termos de testes noturnos. Obviamente, quanto mais rápido os testes forem aprovados, melhor. No momento, o tempo médio de desempenho do SelfTester excede 2 horas;
- Ao executar o SelfTester durante o dia, um desenvolvedor precisa esperar menos pelo resultado, o que aumenta a produtividade de sua força de trabalho.
Foi a aceleração do desempenho que se tornou o motivo de aprimoramentos dessa vez.
Multiencadeamento no SelfTester
O SelfTester foi criado inicialmente como um aplicativo multithread, com a capacidade de testar simultaneamente várias soluções. A única limitação era que você não podia verificar simultaneamente a mesma solução para diferentes versões do Visual Studio, porque muitas soluções precisam ser atualizadas para determinadas versões do Visual Studio antes do teste. Durante o curso, as alterações são introduzidas diretamente nos arquivos dos projetos
.vcxproj , o que gera erros durante a execução paralela.
Para tornar o trabalho mais eficiente, o SelfTester usa um agendador de tarefas inteligente para definir e manter um valor estritamente limitado de encadeamentos paralelos.
O planejador é usado em dois níveis. A primeira é o nível de
soluções , é usada para começar a testar a solução
.sln usando o utilitário
PVS-Studio_Cmd.exe . O mesmo agendador, mas com outra configuração de
grau de
paralelismo , é usado dentro do
PVS-Studio_Cmd.exe (no nível de teste dos
arquivos de origem).
O grau de paralelismo é um parâmetro que indica quantos encadeamentos paralelos precisam ser executados simultaneamente.
Quatro e
oito valores padrão foram escolhidos para o grau de paralelismo do nível de soluções e arquivos, respectivamente. Portanto, o número de threads paralelos nesta implementação deve ser 32 (4 soluções testadas simultaneamente e 8 arquivos). Essa configuração nos parece ótima para o trabalho do analisador em um processador de oito núcleos.
Um desenvolvedor pode definir outros valores do grau de paralelismo, de acordo com o desempenho do computador ou as tarefas atuais. Se um desenvolvedor não especificar esse parâmetro, o número de processadores do sistema lógico será escolhido por padrão.
Nota: vamos supor que lidamos com o grau padrão de paralelismo.
O planejador
LimitedConcurrencyLevelTaskScheduler é herdado de
System.Threading.Tasks.TaskScheduler e refinado para fornecer o nível máximo de paralelismo ao trabalhar sobre o
ThreadPool . Hierarquia de herança:
LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler { .... } PausableTaskScheduler: TaskScheduler { .... }
PausableTaskScheduler permite pausar o desempenho da tarefa e, além disso, o
LimitedConcurrencyLevelTaskScheduler fornece controle intelectual da fila de tarefas e agende seu desempenho, levando em consideração o grau de paralelismo, o escopo das tarefas agendadas e outros fatores. Um planejador é usado ao executar tarefas
LimitedConcurrencyLevelTaskScheduler .
Razões para refinamentos
O processo descrito acima tem uma desvantagem: não é ideal quando se lida com soluções de tamanhos diferentes. E o tamanho das soluções no pool de teste é
muito diversificado: de 8 KB a 4 GB - o tamanho de uma pasta com uma solução e de 1 a vários milhares de arquivos de código-fonte em cada um.
O planejador coloca soluções na fila simplesmente uma após a outra, sem nenhum componente inteligente. Deixe-me lembrá-lo de que, por padrão, não mais de quatro soluções podem ser testadas simultaneamente. Se quatro soluções grandes forem testadas atualmente (o número de arquivos em cada uma é superior a oito), presume-se que trabalhemos efetivamente porque usamos o maior número possível de threads (32).
Mas vamos imaginar uma situação bastante frequente, quando várias pequenas soluções são testadas. Por exemplo, uma solução é grande e contém 50 arquivos (o número máximo de threads será usado), enquanto outras três soluções contêm três, quatro, cinco arquivos cada. Nesse caso, usaremos apenas 20 threads (8 + 3 + 4 + 5). Temos subutilização do tempo do processador e reduzimos o desempenho geral.
Nota : na verdade, o gargalo é geralmente o subsistema de disco, não o processador.
Melhorias
A melhoria que é evidente neste caso é a classificação da lista de soluções testadas. Precisamos obter o uso ideal do número definido de threads executados simultaneamente (32), passando para testar projetos com o número correto de arquivos.
Vamos considerar novamente nosso exemplo de teste de quatro soluções com o seguinte número de arquivos em cada um: 50, 3, 4 e 5. A tarefa que verifica uma solução de
três arquivos provavelmente funcionará mais rapidamente. Seria melhor adicionar uma solução com oito ou mais arquivos em vez dela (para usar o máximo dos threads disponíveis para esta solução). Dessa forma, utilizaremos 25 threads de uma só vez (8 +
8 + 4 + 5). Nada mal. No entanto, sete threads ainda não estão envolvidos. E aqui vem a idéia de outro refinamento, que é remover o limite de quatro threads nas soluções de teste. Porque agora podemos adicionar não uma, mas várias soluções, utilizando 32 threads. Vamos imaginar que temos mais duas soluções de três e quatro arquivos cada. A adição dessas tarefas fechará completamente a "lacuna" de threads não utilizados e haverá 32 (8 + 8 + 4 + 5 +
3 +
4 ) deles.
Espero que a ideia seja clara. De fato, a implementação dessas melhorias também não exigiu muito esforço. Tudo foi feito em um dia.
Precisávamos refazer a classe de tarefa: herdar de
System.Threading.Tasks.Task e atribuir o campo "peso". Utilizamos um algoritmo simples para definir peso para uma solução: se o número de arquivos for menor que oito, o peso será igual a esse número (por exemplo, 5). Se o número for maior ou igual a oito, o peso será igual a oito.
Também tivemos que elaborar o agendador: ensiná-lo a escolher soluções com o peso necessário para atingir o valor máximo de 32 threads. Também tivemos que permitir mais de quatro threads para teste de soluções simultâneas.
Por fim, precisávamos de uma etapa preliminar para analisar todas as soluções no pool (avaliação usando a API do MSBuild) para avaliar e definir o peso das soluções (obter números de arquivos com o código-fonte).
Resultado
Acho que depois de uma introdução tão longa, você já adivinhou que nada aconteceu.
É bom que as melhorias tenham sido simples e rápidas.
Aí vem a parte do artigo, onde vou falar sobre o que "nos causou muitos problemas" e todas as coisas relacionadas a ele.
Efeitos colaterais
Portanto, um resultado negativo também é um resultado. Verificou-se que o número de soluções grandes no pool
excede em muito o número de pequenas (menos de oito arquivos). Nesse caso, essas melhorias não têm um efeito muito perceptível, pois são quase invisíveis: testar pequenos projetos leva uma quantidade minúscula de tempo em comparação ao tempo, necessária para grandes projetos.
No entanto, decidimos deixar o novo refinamento como "não perturbador" 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, a vida acontece. Para evitar que esse erro seja perdido, criamos um incidente interno (ticket) com o nome "Exceção ao trabalhar com o SelfTester". O erro ocorreu ao avaliar o projeto. Embora um grande número de janelas com erros indique o problema de volta no manipulador de erros. Mas isso foi rapidamente eliminado e, na semana seguinte, nada caiu. De repente, outro usuário reclamou do SelfTester. Novamente, o erro de uma avaliação de projeto:
Desta vez, a pilha continha muitas informações úteis - o erro estava no formato xml. É provável que, ao manipular o arquivo do projeto
Proto_IRC.vcxproj (sua representação xml), algo tenha acontecido com o próprio arquivo, por isso o
XmlTextReader não conseguiu lidar com isso.
Ter dois erros em um período bastante curto nos levou a olhar mais de perto o problema. Além disso, como eu disse acima, o SelfTester é usado ativamente pelos desenvolvedores.
Para começar, analisamos a última falha. É triste dizer que não encontramos nada suspeito. Para o caso de pedirmos aos desenvolvedores (usuários do SelfTester) que estejam atentos e relatem possíveis erros.
Ponto importante: o código incorreto foi reutilizado no SelfTester. Foi originalmente usado para avaliar projetos no próprio analisador (
PVS-Studio_Cmd.exe ). É por isso que a atenção ao problema aumentou. No entanto, não houve essas falhas no analisador.
Enquanto isso, o ticket sobre problemas com o SelfTester foi complementado com novos erros:
XmlException novamente. Obviamente, existem threads concorrentes em algum lugar que trabalham com a leitura e gravação de arquivos de projeto. O SelfTester trabalha com projetos nos seguintes casos:
- Avaliação de projetos no curso do cálculo preliminar dos pesos das soluções: uma nova etapa que inicialmente despertou suspeitas;
- A atualização de projetos para as versões necessárias do Visual Studio: é executada imediatamente antes do teste (os projetos não interferem) e não deve afetar o processo de trabalho.
- Avaliação de projetos durante o teste: um mecanismo bem estabelecido para segurança de threads, reutilizado no PVS-Studio_Cmd.exe ;
- Restaurando arquivos de projeto (substituindo arquivos .vcxproj modificados por arquivos de referência iniciais) ao sair do SelfTester, porque os arquivos de projeto podem ser atualizados para as versões necessárias do Visual Studio durante o trabalho. É uma etapa final, que não tem impacto em outros mecanismos.
A suspeita recaiu sobre o novo código adicionado para otimização (cálculo de peso). Mas sua investigação de código mostrou que, se um usuário executa a análise logo após o início do SelfTester, o testador sempre espera corretamente até o final da pré-avaliação. Este lugar parecia seguro.
Mais uma vez, não conseguimos identificar a fonte do problema.
Dor
Durante todo o mês seguinte, o SelfTester continuou travando repetidamente. O ticket continuava sendo preenchido 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 .
Tradicionalmente, as ferramentas internas não são impostas requisitos muito altos, por isso continuamos confundindo os erros do SelfTester com um princípio residual. De tempos em tempos, pessoas diferentes se envolviam (durante todo o incidente, seis pessoas trabalhavam no problema, incluindo dois estagiários). No entanto, tivemos que nos distrair com essa tarefa.
Nosso primeiro erro. De fato, neste ponto, poderíamos ter resolvido esse 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 claramente não pode 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ê provavelmente entende, isso não foi feito. Continuamos trabalhando no problema que criamos a nós mesmos. Continuamos procurando a resposta: "COMO ???" Como ele falha? Parecia estar escrito corretamente.
Nosso segundo erro. Outras pessoas se envolveram na solução do problema
. É um erro muito, muito grande. Não apenas resolveu o problema, mas também exigiu recursos adicionais desperdiçados. Sim, novas pessoas trouxeram novas idéias, mas demorou muito tempo para implementar (à toa) essas idéias. Em algum momento, nossos estagiários escreveram programas de teste emulando a avaliação de um e do mesmo projeto em threads diferentes, com modificação paralela de um projeto em outro projeto. Isso não ajudou. Descobrimos apenas que a API do MSBuild era segura para threads por dentro, o que já sabíamos. Também adicionamos o salvamento automático de mini despejo quando ocorre a exceção
XmlException . Tínhamos alguém que estava depurando tudo isso. Pobre rapaz! Houve discussões, fizemos outras coisas desnecessárias.
Finalmente, terceiro erro. Você sabe quanto tempo se passou desde o momento em que o problema do SelfTester ocorreu até o momento em que foi resolvido? Bem, você pode contar a si mesmo. O ingresso foi criado em 17/09/2018 e encerrado em 20/02/2019. Foram mais de 40 comentários! Gente, isso é muito tempo! Nós
nos permitimos ficar ocupados por cinco meses com ISTO. Ao mesmo tempo, estávamos ocupados dando suporte ao Visual Studio 2019, adicionando o suporte à linguagem Java, introduzindo o padrão MISRA C / C ++, melhorando o analisador C #, participando ativamente de conferências, escrevendo vários artigos, etc. Todas essas atividades receberam menos tempo dos desenvolvedores devido a um erro estúpido no SelfTester.
Gente, aprenda com nossos erros e nunca faça isso. Nós também não.
É isso, eu terminei.
Ok, foi uma piada, eu vou te contar qual foi o problema com o SelfTester :)
Bingo!
Felizmente, havia uma pessoa entre nós com visão nítida (meu colega Sergey Vasiliev), que apenas olhou o problema de um ângulo muito diferente (e também - ele teve um pouco de sorte). E se estiver tudo bem dentro do SelfTester, mas algo externo trava os projetos? Normalmente, não lançamos nada com o SelfTester; em alguns casos, controlamos rigorosamente o ambiente de execução. Nesse caso, esse "algo" poderia ser o próprio SelfTester, mas uma instância diferente.
Ao sair do SelfTester, o encadeamento que restaura os arquivos do projeto a partir de referências continua trabalhando por um tempo. Nesse ponto, o testador pode ser iniciado novamente. A proteção contra execuções simultâneas de várias instâncias do SelfTester foi adicionada
posteriormente e agora tem a seguinte aparência:
Mas naquele momento não tínhamos.
Porcas, mas é verdade - durante quase seis meses de tormento, ninguém prestou atenção a isso. Restaurar projetos a partir de referências é um procedimento em segundo plano bastante rápido, mas infelizmente não é rápido o suficiente para não interferir no relançamento do SelfTester. E o que acontece quando o lançamos? É isso mesmo, calculando os pesos das soluções. Um processo reescreve arquivos
.vcxproj enquanto outro tenta lê-los. Diga oi para
XmlException .
Sergey descobriu tudo isso quando adicionou a capacidade de alternar para um conjunto diferente de logs de referência no testador. Tornou-se necessário após adicionar um conjunto de regras MISRA no analisador. Você pode alternar diretamente na interface, enquanto o usuário vê esta janela:
Depois disso, o
SelfTester é reiniciado. E antes, aparentemente, os usuários de alguma forma imitaram o problema, executando o testador novamente.
Blamestorming e conclusões
Obviamente, removemos (ou seja, desativamos) a otimização criada anteriormente. Além disso, era muito mais fácil do que fazer algum tipo de sincronização entre as reinicializações do testador por si só. E tudo começou a funcionar perfeitamente, como antes. E como uma medida adicional, adicionamos a proteção acima contra o lançamento simultâneo do testador.
Eu já escrevi acima sobre nossos principais erros ao procurar o problema, o suficiente para auto-flagelação. Somos seres humanos, então podemos estar errados. É importante aprender com seus próprios erros e tirar conclusões. As conclusões deste caso são bastante simples:
- Devemos monitorar e estimar a complexidade da tarefa;
- Às vezes, precisamos parar em algum momento;
- Tente analisar o problema de maneira mais ampla. Com o tempo, é possível obter uma visão em túnel do caso, enquanto isso requer uma nova perspectiva.
- Não tenha medo de excluir código antigo ou desnecessário.
É isso, desta vez eu definitivamente estou pronto. Obrigado por ler até o fim. Desejo-lhe código sem erros!