Andre Nobre

Desenvolvedor e arquiteto. Apaixonado por tech, código limpo e simplificação dos discursos tecnológicos.

Mês: agosto 2019

Começando com WinDBG

Há mais de 10 anos eu escrevo e falo sobre debugging. Praticamente todos as palestras, artigos e demonstrações com maior complexidade eu utilizo o WinDBG.

Comecei a escrever um guia em 2008 em um antigo blog, mas infelizmente parei.

Ontem em um evento com duas palestras sobre .NET avançado / internals, vi novamente o WinDBG sendo apresentado e o espanto interesse de diversas pessoas sobre o potencial da ferramenta.

Resolvi então escrever um guia rápido para iniciantes, com o intuito de gerar o interesse e detalhar o caminho para os próximos passos.

O que é WinDBG?

Da fonte oficial,

“The Windows Debugger (WinDbg) can be used to debug kernel-mode and user-mode code, to analyze crash dumps, and to examine the CPU registers while the code executes.”

https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools

Talvez as primeiras definições que gerem dúvidas para alguns leitores são estas de código kernel-mode e user-mode. Falamos com certa frequência estes termos, então é importante garantirmos o entendimento.

De forma bem simples, objetiva e direta, aplicações rodam em user-mode, e componentes do sistema operacional rodam em kernel mode.

Portanto, para nossos cenários de investigação de problemas em .NET, vamos realizar o user-mode debugging.

Quando você quiser, entre em mais detalhes através deste link.

Faça o download do WinDBG. Se quiser ir para a versão mais nova (preview neste momento) é interessante, ela fornece uma interface “menos assustadora” e mais intuitiva.

Realizando o debug a partir de um dump

Com o WinDBG você pode realizar o debugging de diversas formas. A primeira e – possivelmente a mais comum para o mundo de aplicações gerenciadas – é através de um dump de memória.

Existem alguns tipos de memory dumps, mas para nosso cenário precisamos extrair este dump de um processo, se tratando portanto de um user dump.

Este dump nada mais é que um snapshot de um processo ou sistema em um determinado momento. Imagine então que determinar o momento correto para extração desta “foto” é essencial para análise dos problemas.

É possível associar esta extração a alguma determinada situação, como por exemplo um uso excessivo de CPU por um determinado tempo. Desta maneira conseguimos garantir que teremos a foto do exato momento de um cenário passível de investigação.

A maneira mais fácil e simples de extrair um dump é através do gerenciador de tarefas. Encontrando o processo desejado, clique com o botão direito e vá até a opção “Criar arquivo de despejo” em português ou “Create dump file” em inglês:

O processo indicará o caminho do dump gerado.

O mais comum é gerar dumps através de ferramentas como procdump.exe (sysinternals) ou adplus.exe (faz parte do pacote de Debugging Tools for Windows).

Eu recomendo através de procdump por ser muito mais “portável”. Faça o download por aqui e siga os exemplos neste mesmo link.

Iniciando a investigação

Considerando que você já extraiu o dump do seu processo (w3wp, iisexpress, etc), você deverá abrir o WinDBG e abrir o dump por lá. Basta ir até “Arquivo” e optar por “Open crash dump” ou “Open dump”, dependendo da versão do seu WinDBG.

A primeira tela que você terá contato deve ser como esta (clique para ampliar):

Esta tela já traz algumas informações importantes, como o ambiente de onde o dump foi extraído, a ferramenta utilizada, momento, etc.

Lá embaixo temos a barra de comando, onde colocaremos tudo que gostaríamos de analisar.

É importante saber que por default o WinDBG não conhece .NET. Para isto, precisamos “carregar” uma extensão extremamente conhecida, a SOS.dll.

Esta extensão disponibiliza alguns comandos e “traduz” a CLR para o WinDBG.

O runtime do .NET Framework (full ou core) traz esta extensão por padrão. No meu caso, fiz o download do runtime da versão do processo para que pudesse trabalhar.

Com o SOS.dll em mãos basta carregar na sessão de debug (meu SOS.dll esta na pasta que extraí do donwload do SDK):

.load C:\Users\81298\Downloads\dotnet-sdk-2.2.301-win-x86\shared\Microsoft.NETCore.App\2.2.6\sos.dll

A partir deste momento posso utilizar alguns comandos conhecidos que estão disponíveis pela extensão SOS.

Quais comandos executar?

Isso depende muito do que você está precisando investigar. Mas para ter ideia do potencial, você pode, por exemplo, visualizar todas as threads, o heap, os objetos na memória, etc.

É importante saber que através de uma ferramenta como está é possível enxergar tudo, absolutamente tudo.

Por exemplo:

Lista todas as threads:

0:018> !threads
ThreadCount:      24
UnstartedThread:  0
BackgroundThread: 22
PendingThread:    0
DeadThread:       1
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 1d94 03855440   202a020 Preemptive  00000000:00000000 037fd060 0     MTA 
   3    2 2014 0a21cd28     2b220 Preemptive  00000000:00000000 037fd060 0     MTA (Finalizer) 
XXXX    3    0 038ea538     39820 Preemptive  00000000:00000000 037fd060 0     Ukn 
   4    5 2b2c 0a234a10   102a220 Preemptive  00000000:00000000 037fd060 0     MTA (Threadpool Worker) 
   5    7 2854 0a27e3a0   202b220 Preemptive  00000000:00000000 037fd060 0     MTA 
   6    8 2210 0a2a3530   202b220 Preemptive  00000000:00000000 037fd060 1     MTA 
   7    9 1960 036fee70   202b220 Preemptive  00000000:00000000 037fd060 0     MTA 
   8   12 2c10 0a2f2f08     2b220 Preemptive  00000000:00000000 037fd060 0     MTA 
   9   13 25b4 0a2fb908     2b220 Preemptive  00000000:00000000 037fd060 0     MTA 
  11   10 2478 0d72ea58     21220 Preemptive  00000000:00000000 037fd060 0     Ukn 
  12   32 35d8 0d7dd818   8029220 Preemptive  00000000:00000000 037fd060 0     MTA (Threadpool Completion Port) 
  13   27 2598 0d902f58     20220 Preemptive  12B4B7E8:00000000 037fd060 0     Ukn 
  14   55 38dc 0d8fdad8     20220 Preemptive  00000000:00000000 037fd060 0     Ukn 
  15   56 3e24 0d904478   1029220 Preemptive  12B4B274:00000000 037fd060 0     MTA (Threadpool Worker) 
  16   53 3938 0d901f80   1029220 Preemptive  12B4AAC8:00000000 037fd060 0     MTA (Threadpool Worker) 
  18   42 3944 0d7de7f0   1029220 Preemptive  12B4B3E0:00000000 037fd060 0     MTA (Threadpool Worker) 
  20    6 3dd8 0d7ddd60   1029220 Preemptive  12B4B13C:00000000 037fd060 0     MTA (Threadpool Worker) 
  21   43 3084 0d7dc840   1029220 Preemptive  12B4AD44:00000000 037fd060 0     MTA (Threadpool Worker) 
  22   46 3ac0 0d7df7c8   1029220 Preemptive  12B4B65C:00000000 037fd060 0     MTA (Threadpool Worker) 
  23   31 3c70 0d7dcd88   1029220 Preemptive  12B4AE70:00000000 037fd060 0     MTA (Threadpool Worker) 
  24   45 3590 0d7db868   1029220 Preemptive  12B4AC18:00000000 037fd060 0     MTA (Threadpool Worker) 
  25   41 36a4 0d7dfd10   1029220 Preemptive  12B4C6A4:00000000 037fd060 0     MTA (Threadpool Worker) 
  26   50 3a34 0d7d8e28   1029220 Preemptive  12B4AFA8:00000000 037fd060 0     MTA (Threadpool Worker) 
  27   49 2c84 0d7de2a8   1029220 Preemptive  12B49900:00000000 037fd060 0     MTA (Threadpool Worker) 

Mostra o stack da thread selecionada:

0:018> !clrstack
OS Thread Id: 0x3944 (18)
Child SP       IP Call Site
0edce25c 77afed0c [HelperMethodFrame: 0edce25c] 
0edce2c8 6541bcca System.String.Concat(System.Object[])
0edce2fc 0cc62eb0 BuggyBits.Controllers.AllProductsController.Index()
0edce34c 0ae9322b LoadSymbols moduleData.Request FAILED 0x80004005
DynamicClass.lambda_method(System.Runtime.CompilerServices.Closure, System.Object, System.Object[])
0edce358 0e05d38e ...

Informações sobre GC e objetos no HEAP:

0:018> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class Name
6574a19c        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.Int32, System.Private.CoreLib]]
6574820c        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.Int64, System.Private.CoreLib]]
65742f6c        1           12 System.Buffers.TlsOverPerCoreLockedStacksArrayPool`1+PerCoreLockedStacks[[System.Byte, System.Private.CoreLib]]
657428d0        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.UInt64, System.Private.CoreLib]]
...

Existem diversos outros comandos, veja aqui a lista.

Próximos passos

Se você realiza trabalhos de troubleshooting ou gostaria de entender melhor o funcionamento de baixo nível do seu processo, entre mais a fundo em WinDBG e user mode debugging.

Recomendo muito o Debugging Labs da Tess. Ela não escreve e acho que nem trabalha mais com isto, mas para quem está começando pode dar uma ideia sensacional de como evoluir.

Existem livros interessantes também. Um deles (antigo, mas essencial) é o Advanced .NET Debugging. Não tem problema ser antigo; o mais relevante são as instruções, o método e os comandos relacionados não ao framework .NET, mas sim ao WinDBG.

Fique a vontade para entrar em contato para falar sobre.

Abraços, até a próxima!

Investigação de problemas em App Services

Hoje é muito simples e rápido subir um workload no Azure. Com qualquer artigo e how-to por aí é possível – em questão de minutos – ter algo rodando na nuvem.

Normalmente este é um motivo de alegria, que se transforma em algo desafiador logo no primeiro problema. Não ter acesso ao ambiente de execução com uma certa liberdade torna as atividades de análise e diagnóstico mais complicadas.

O objetivo deste artigo é apresentar algumas maneiras de investigar possíveis problemas em app services publicados no Azure.

Abordagens

Temos algumas maneiras de realizar as investigações de um app service, desde utilizando interface de maneira intuitiva até ferramentas avançadas.

A primeira abordagem é através do menu Diagnose and solve problems. Como o nome mesmo diz, esta funcionalidade permite realizar uma análise macro sobre qualquer problema que possa ter sido percebido.

Menu Diagnose and solve problems

Em qualquer investigação precisamos entender o que está acontecendo com alguns indicadores básicos. Uma vez na página de Diagnose and solve problems é possível acessar a seção Diagnostic Tools:

Página Diagnostic Tools

Dentro do grupo Support Tools, os itens de métricas (Metrics per Instance (Apps) e (App Service Plan) podem ajudar a entender alguns comportamentos.

É possível por exemplo visualizar as coleções do GC, o consumo de CPU, threads, entre outras informações:

Métricas de limpezas realizadas na geração 0, 1 e 2 pelo GC

“Descendo o nível”, uma boa opção é a Proactive CPU Monitoring. Se trata de um conjunto de ações para realizar dado um determinado comportamento de consumo de CPU.

Uma vez que estas características estejam ocorrendo, um dump será gerado e você poderá investigar tudo que quiser através dele.

Em um aplicação de testes, realizei a configuração para extrair um dump caso a CPU estivesse com consumo maior que 75% por mais de 30 segundos.

Assim que as exigências foram atendidas, o dump foi gerado.

Basta agora realizar a análise do dump utilizando a ferramenta de preferência, como por exemplo WinDBG:

WinDBG analisando dump gerado pelo portal do Azure

Notem que a geração do dump foi feita através do procdump.exe. Você também pode executar este comando através do menu Development Tools do seu app service.

Menu Development Tools no seu app service

Ao acessar o Kudu (Advanced Tools), você tem acesso a configurações, informações e ambiente de execução do seu aplicativo.

Todas as ferramentas do Sysinternals estão disponíveis. Para executar um procdump, por exemplo, basta acessar o Debug Console e chamar o executável:

Advanced Tools (Kudu) executando o procdump no Debug Console

Conclusão

É muito comum encontrar pessoas que estão começando a utilizar PaaS com a preocupação da falta de controle sobre o ambiente de execução.

Porém, o Azure permite que você tenha acesso a ferramentas comuns de análise e investigação para que suas atividades de troubleshooting possam ser executadas normalmente.

Como o artigo é superfial, qualquer dúvida adicional por favor entrem em contato.

Abraços, até a próxima!

Analisando a complexidade de algoritmos

Existem alguns fundamentos básicos de algoritmos que todos aqueles que se envolvem com isto deveriam saber.

Um deles é entender o quão complexo é um algoritmo, e principalmente se há alternativas melhores.

Para isto existem notações e métodos conhecidos na literatura.

O objetivo deste post é tentar explicar de maneira clara a notação Big-O.

Começando a entender a notação

Notação

Substantivo feminino

1.ato de notar, de representar algo por meio de símbolos ou caracteres.

Primeiro ponto que precisamos chegar a um acordo: não podemos medir complexidade e performance (eficiência) baseada em tempo (milissegundos, segundos, minutos, etc) através de um programa rodando na sua própria máquina.

O tempo final de um algoritmo é relativo à capacidade do seu ambiente de execução. O que eu quero dizer com isto é que se você tem um ambiente ruim, seu algoritmo demorará mais tempo pra ser executado.

De forma resumida, é por este motivo que não podemos entender a complexidade de um algoritmo pelo tempo que ele executa em uma determinada estrutura.

Por isto partimos de um pressuposto que a gente irá aferir nossa complexidade através de medidas que independem do runtime, através de uma notação.

Podemos criar diversas maneiras de medir esta complexidade e chegar a um padrão. Porém, a comunidade acadêmica matemática já fez isso há bastante tempo, criando a notação Big-O.

A notação Big-O demonstra a complexidade de um algoritmo em referência à suas entradas. Por exemplo, se um algoritmo executa apenas 1 instrução, e sempre executará independente do valor da sua entrada, dizemos que a complexidade deste algoritmo é O(1), ou constante.

Veja um exemplo muito simples:

static int SumNumbers(int a, int b) 
{ 
  return a + b; 
} 

Analise que independente dos valores de a e b, este algoritmo sempre executará 1 instrução.

Podemos ter casos mais complexos como O(n). Esta notação indica um crescimento linear de acordo com a entrada.

Veja o exemplo abaixo:

static bool FindItem(List<string> items, string value) 
{ 
  foreach(var item in items) 
  { 
    if (item == value) 
    { 
      return true; 
    } 
  } 
  return false; 
} 

Este bloco aceita uma lista de entrada e um valor para ser encontrado. Vamos considerar que a lógica interna (if…) é executada uma vez para cada item da lista.

Se adotarmos que cada execução demora “1 tempo” pra ser executado, se enviarmos 10 elementos teremos “10 tempos”; se enviarmos 100 elementos teremos “100 tempos” e assim por diante.

Veja que o crescimento é linear; se em um determinado ambiente de execução esta unidade “tempo” for igual a 1 segundo, cada elemento adicionado na lista soma 1 segundo ao tempo total.

Por isto a notação O(n) é linear em relação à entrada da função versus tempo de execução.

No gráfico podemos ver:

O que podemos entender por este início?

Espero que neste ponto tenha sido possível compreender que a notação Big-O demonstra a complexidade de um algoritmo criando uma referência em relação ao número de execução das instruções de acordo com o tamanho da entrada.

Se um algoritmo, independente da sua entrada, sempre executa apenas 1 instrução, dizemos que a complexidade é O(1). Se a quantidade de instruções executadas cresce na mesma proporção que a sua entrada, dizemos então que é O(n).

Avançando um pouco mais

A lógica da notação é sempre a mesma. Se você tem um algoritmo que a complexidade cresce em uma proporção log n de acordo com sua entrada, então dizemos que sua complexidade é O(log n)

Um exemplo clássico de um algoritmo O(log n) é uma busca binária

Veja:

static int binarySearch(int[] nums, int startingIndex, int length, int itemToSearch) 
{ 
  if (length >= startingIndex) 
  { 
    int mid = startingIndex + (length - startingIndex) / 2; 
 
    // If the element found at the middle itself 
    if (nums[mid] == itemToSearch) 
    return mid; 
 
    // If the element is smaller than mid then it is 
    // present in left set of array 
    if (nums[mid] > itemToSearch) 
    return binarySearch(nums, startingIndex, mid - 1, itemToSearch); 
 
    // Else the element is present in right set of array 
    return binarySearch(nums, mid + 1, length, itemToSearch); 
  } 
 
  // If item not found return 1 
  return -1; 
} 

Por que este algoritmo é O(log n)?

Primeiro vamos entender do se trata este algoritmo.

O objetivo é encontrar um número dentro de uma lista de n números.

Uma maneira muito fácil de executar isto é percorrer todos os números da lista, realizando a comparação sobre cada item.

No pior cenário, vamos realizar a análise sobre TODOS os elementos. Isso quer dizer que se a lista tiver 100 elementos, vamos realizar a comparação sobre os 100 para encontrar o que estamos procurando (no pior cenário).

Então desta forma o algoritmo teria complexidade O(n).

Usando uma busca binária podemos encontrar o elemento procurado de forma muito mais fácil.

Através de divisão e conquista, o algoritmo busca dividindo a lista (considerando que está ordenada) e realizando a pesquisa apenas no bloco onde o elemento provavelmente está, ate que encontre.

Isso quer dizer que nunca iremos percorrer todos os elementos da lista, garantindo então que a complexidade deste algoritmo não cresce de forma linear com o número de elementos (portanto, de fato não é O(n)).

Vamos considerar esta lista para nosso exemplo: {1, 5, 6, 10, 15, 17, 20, 42, 55, 60, 67, 80, 100}

Supondo que queremos o número 55, em quantas execuções conseguiremos encontrar o resultado?

Faça o teste de mesa do algoritmo. O resultado será 3 execuções!

Se utilizássemos algum algoritmo O(n) poderíamos levar até 13 execuções.

Aqui você pode se perguntar: mas se o elemento a ser buscado está no inicio do conjunto, a busca linear é muito mais performática.

E você está de certo. Mas é fato que não podemos lidar com algo tão variável como este. E por isto é importante saber de um conceito importante sobre Big-O:

Esta notação representa o limite superior de uma função, dado uma entrada extremamente grande (ou com esta tendência).

Isso quer dizer que, dado uma entrada n consideravelmente grande, o comportamento da função segue o resultado da notação.

No nosso caso, com um crescimento linear, caso a lista n seja extremamente grande (por exemplo, 1 bilhão de registros), poderíamos levar até 1 bilhão de comparações para encontrar o número desejado, enquanto com um algoritmo log n levaríamos em torno de 31 execuções.

Podemos ver a performance de um algoritmo O(log n) através do gráfico abaixo:

Como analisar um código da minha solução?

Algo muito comum de se encontrar na literatura é a análise de complexidade utilizando Big-O sobre algoritmos padrão.

Para analisar sobre seu algoritmo do dia a dia, a lógica é a mesma. Vamos ver este exemplo abaixo:

 var listaNovosRelacionamentos = modalidadesCategoria.Where(filtro => filtro.Selecionado == true).ToList();
                foreach (var modalidade in listaNovosRelacionamentos)
                {
                    try
                    {
                        var modelosAtualizar = relacionados.Where(a => a.CategoriaId == modalidade.CategoriaId && a.ModeloId == codigoModelo && a.ModalidadeId == modalidade.ModalidadeId);

                        foreach (var modeloAtualizar in modelosAtualizar)
                        {
                            if (!modeloAtualizar.FlagPrincipal || (modeloAtualizar.FlagPrincipal && modalidade.Selecionado))
                            {
                                modeloAtualizar.DataAtualizacao = DateTime.Now;
                                modeloAtualizar.FlagExcluido = !modalidade.Selecionado;
                                ModeloCategoriaModalidade.Atualizar(modeloAtualizar);
                            }
                            else
                            {
                                status.AdicionarErro("Não é possível desvincular a modalidade principal!");
                            }

                        }

                        if (!modelosAtualizar.Any())
                        {
                            if (modalidade.Selecionado)
                            {
                                var modeloNovo = new ModeloCategoriaModalidade();
                                modeloNovo.ModeloId = codigoModelo;
                                modeloNovo.ModalidadeId = modalidade.ModalidadeId;
                                modeloNovo.CategoriaId = modalidade.CategoriaId;
                                modeloNovo.DataCriacao = DateTime.Now;
                                modeloNovo.DataAtualizacao = DateTime.Now;
                                modeloNovo.FlagExcluido = false;

                                ModeloCategoriaModalidade.Inserir(modeloNovo);
                            }

                        }

                    }
                    catch
                    {
                        status.AdicionarErro("Erro ao atualizar a modalidade " + modalidade.ModalidadeId + " !");
                    }
                }

Analise o trecho de código acima. Qual é a complexidade deste algoritmo?

Mesmo sem saber os detalhes, é muito possível que este algoritmo seja O(n²). Dado uma lista de entrada, ela percorre todos os itens e ainda efetua uma lógica sobre itens relacionados a cada entrada.

Quer dizer que se o item listaNovosRelacionamentos for extremamente grande, a tendência é que o tempo deste algoritmo seja completamente inviável.

Veja o gráfico referente a um algoritmo n²:

O exercício é: será que é possível melhorar a eficiência deste algoritmo sabendo disto?

Conclusão

É extremamente importante entender os conceitos básicos de algoritmos e também de estrutura de dados para trabalhar com isto no dia a dia.

A notação Big-O é muito importante para nos tornarmos mais profissionais. Evolua para outras notações como Omega-Q, Theta-O

Espero ter conseguido passar de uma forma mais introdutória este conceito. Em breve entrarei com novos posts para a continuação do assunto.

Grande abraço!

Desenvolvido em WordPress & Tema por Anders Norén