Resposta ao Desafio da Semana #7 [Hang de Alta CPU em C#/VB.NET]

Por: Roberto Alexis Farah

Eis a resposta ao Desafio da Semana 7.

Veja o link com a definição do desafio aqui

PROBLEMA

O problema é que o código erroneamente assumia que a string recebida como parâmetro sempre tinha um número.

Portanto, o loop varre os caracteres da string até encontrar um número, quando não há número o loop nunca é terminado, ou seja, loop infinito causando alto consumo de CPU.

Note que além do problema de se assumir que a string continha números, o código também não preve a situação onde a string está vazia, ocasionando o mesmo loop infinito!

Assim, embora o código funcione na maioria das vezes ele vai falhar quando a string for vazia ou não houver números dentro dela, gerando o sintoma de alta CPU.

 

SOLUÇÃO

Mesmo sem ter uma especificação sobre o que o código deveria fazer e como deveria reagir é possível se concluir que, se deveria processar strings sem número, ele não faz isso. E se deveria rejeitar strings sem números, ele também não faz isso.

No caso, supondo-se que a idéia seja de se evitar strings sem números (e vazias), uma possível solução seria:

     'Exemplo de parametros:

     'ABC!DEF#45GH

     'ABC!DEFxxxxAAAGH1

     '4fgfgjfdhg3fdgjfgjh1

     Public Function Crypto(ByVal someString As String) As String

              ' Inicializa a semente de nuvero aleatorio para sempre ser diferente.

              VBMath.Randomize()

              ' FIX: Verifica se ha numeros na string.

              ' Tambem evita a situacao de string vazia!

              ' Abordagem pouco invasiva, nao foi necessario se alterar o algoritmo original.

' FIX 2: Antes testa se string e´objeto NULL.

              If someString = Nothing OrElse Not Regex.IsMatch(someString, "[0-9]") Then

                   Debug.Assert(True, "someString nao tem caracteres numericos")

' Pode-se logar mensagem no Event Viewer antes…

                   Return vbNullString

              End If

              Dim num1 As Long = Strings.Len(someString)

              ' Cria uma chave usando um numero aleatorio e o tamanho da string.

              ' O numero nunca e' maior que o tamanho da string.

              Dim text1 As String = StringType.FromLong(CType(Math.Round(CType(((num1 * VBMath.Rnd) + 1.0!), Double)), Long))

              ' Varre os caracteres da string apenas.

              Do While Not Char.IsNumber(CharType.FromString(Strings.Mid(someString, IntegerType.FromString(text1), 1)))

                   ' Efetua a mesma conversao.

                   text1 = StringType.FromLong(CType(Math.Round(CType(((num1 * VBMath.Rnd) + 1.0!), Double)), Long))

              Loop

              Crypto = text1

     End Function

Como curiosidade explicarei como o problema foi isolado no cenário real. Primeiro foi necessário se identificar via Performance Monitor quem eram os processos e threads consumindo alta CPU.

Em seguida coletei um Hang Dump do processo. A análise automatizada revelou a pilha (stack) das threads executando o código suspeito e, em seguida, disparando o Garbage Collector.

A primeira vista alguns pensaram que era um problema no GC. Na verdade, o GC estava sendo disparado justamente pelo número de threads travadas no loop.

Após isso, depurei o dump manualmente e extrai o assembly a partir do dump e converti o código do método suspeito para VB.NET.

O código era bastante complexo com muitas operações e conversões. Entretanto, via o dump foi possível se extrair a string usada como parâmetro, analisar o código e entender como a string seria processada. A string não continha caracteres numéricos.

Como recomendação embasada por diversos livros de programação;depuração e fazendo minhas as palavras do meu amigo Waldemir Cambiucci, consultor senior da MCS em São Paulo, coloco: Nunca assuma!

Nunca assuma quando investigando um problema e nunca assuma quando programando. Para isso, quando você considera que uma situação nunca deveria ocorrer, use Assert, seja em VB.NET, C++, C ou C# para que você receba uma notificação se o que você assumiu como verdadeiro for falso (modo Debug) e eventualmente você pode prevenir problemas em versões Release testando a condição e registrando uma mensagem no Event Viewer, por exemplo.

Programando proativamente, usando Assert e trace (logs) ajuda a se evitar bugs e a pegá-los mais rapidamente.

No código original, o simples uso de Assert teria feito o problema ser identificado quando testado em modo debug e o uso de mensagens de trace teria tornado fácil identificar o problema em ambiente de produção.

Eis alguns links sobre o assunto:

https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vsdebug/html/_core_Debugging_Assertions.asp

https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vsdebug/html/vxtskverifyingassertionsinmanagedcode.asp

https://msdn.microsoft.com/msdnmag/issues/01/10/bugslayer/

https://msdn.microsoft.com/msdnmag/issues/05/11/Bugslayer/default.aspx