Desenvolvimento - C#

Monitorando o consumo de memória e tempo de execução

Descrevo uma técnica simples para mensuração do consumo de memória e tempo de execução de um bloco de código para uma determinada AppDomain.

por Elemar Rodrigues Severo Junior



Olá pessoal, tudo certinho?

Essa é a primeira vez que escrevo aqui no “Linha de Código”. Este post também está disponível em meu blog: HTTP://elemarjr.wordpress.com

Sou um tantinho viciado em performance. Meu maior desejo é que meus aplicativos, além de funcionar, façam tudo que precisa ser feito no menor tempo possível e consumindo o mínimo possível de recursos.

O que vou apresentar hoje é um tipo simples que utilizou para testar minhas rotinas. Na prática, o que uso é o recurso de monitoramento da AppDomain.

Sem mais delongas, go code!

O problema

Para ter certeza de que meus códigos estejam funcionando de forma apropriada, verifico, geralmente, três aspectos:

1. tempo de execução de uma rotina;

2. quantidade de bytes alocados;

3. quantidade de bytes que continuam “presos” após a coleta de lixo.

Para dar um exemplo prático, examinemos o seguinte código:

 // alocando objetos em memória que não morrem;

var list = new List<Object>();

for (int i = 0; i < 512; i++)

    list.Add(new byte[1024]);

// objetos criados que morrem logo depois

for (int i = 0; i < 10000; i++)

    (new byte[1024]).GetType();

// parando 2 segundos

int stop = Environment.TickCount + 5000;

while (Environment.TickCount < stop) ;

Esse código não faz nada de útil. Bem, quase nada! Descrição: Smiley piscando

A primeira parte desse código aloca memória não liberada. A segunda, cria uma série maior de objetos que não ficam armazenados (devem ser coletados pelo garbage collector). A terceira, para a execução do código por aproximadamente 5segs.

O que há disponível para medir tempo e consumo de recursos?

Dentro do Framework, podemos encontrar uma série de formas e mecanismos para instrumentar nossos aplicativos e obter informações relacionadas a consumo de recursos e tempos de execução. Entretanto, um dos recursos que mais gosto são as propriedades Monitoring* da AppDomain. Por quê? Por me permitir controle por AppDomain (olha aí, assunto para outro post). Mas que propriedades são essas? Vejamos:

1. MonitoringTotalProcessorTime;

2. MonitoringTotalAllocatedMemorySize;

3. MonitorSurvivedMemorySize.

Ou seja, o tempo total do processador para o appdomain, além do total de memória alocada e da quantidade de memória alocada por objetos sobreviventes na coleta de lixo.

Mantendo um registro de memória e tempo para um determinado bloco de código

Bem, a classe AppDomain nos dá propriedades importantes para que possamos ter uma idéia do que aconteceu em nosso domínio de aplicação desde que este foi criado. Mas se desejarmos saber o efeito de um bloco de código (como o apresentado no início)? Bom, aí começa a minha solução: salvamos os dados no início da execução para poder fazer um “delta” no fim:

public class AppDomainMonitor

{

    public AppDomainMonitor (AppDomain targetAppDomain = null)

    {

        AppDomain.MonitoringIsEnabled = true;

        this.TargetAppDomain =

            targetAppDomain ?? AppDomain.CurrentDomain;

        this.Reset();

    }

    public AppDomain TargetAppDomain { get; private set; }

    private TimeSpan InitialProcessorTimeField;

    private long InitialAllocatedMemorySizeField;

    private long InitialSurvivedMemorySize;

    public void Reset()

    {

        this.InitialProcessorTimeField =

            this.TargetAppDomain.MonitoringTotalProcessorTime;

        this.InitialAllocatedMemorySizeField =

            this.TargetAppDomain.

            MonitoringTotalAllocatedMemorySize;

        this.InitialSurvivedMemorySize =

            this.TargetAppDomain.MonitoringSurvivedMemorySize;

    }

    public AppDomainMonitorSnapshot TakeSnapshot()

    {

        return new AppDomainMonitorSnapshot(this);

    }

    public struct AppDomainMonitorSnapshot

    {

        // ...

    }

}

O que eu fiz? Nada demais. Na prática, fiz uma “cola” dos valores das propriedades Monitoring* em atributos do meu tipo. Observe que adicionei um método “Reset” para não precisar criar outro registro.

Meu construtor é piedoso Descrição: Alegre. Se o usuário informar o AppDomain a ser monitorado, tudo bem. Se não, tudo bem também, pego o AppDomain atual.

O método TakeSnapshot é o responsável por retornar um “resumo” do que aconteceu. Observe que utilizo uma struct aninhada para representar os dados. Aliás, aqui está o código dessa struct:

public struct AppDomainMonitorSnapshot

{

    public AppDomainMonitorSnapshot

        (AppDomainMonitor m) : this()

    {

        if (m == null)

            throw new ArgumentNullException();

        GC.Collect();

        this.AppDomainFriendlyName =

            m.TargetAppDomain.FriendlyName;

        this.ProcessorTimeMs =

            (

            m.TargetAppDomain.MonitoringTotalProcessorTime

            - m.InitialProcessorTimeField

            )

            .TotalMilliseconds;

        this.AllocatedMemorySize =

            m.TargetAppDomain.MonitoringTotalAllocatedMemorySize

            - m.InitialAllocatedMemorySizeField;

        this.SurvivedMemorySize =

            m.TargetAppDomain.MonitoringSurvivedMemorySize

            - m.InitialSurvivedMemorySize;

    }

    public string AppDomainFriendlyName

    { get; private set; }

    public double ProcessorTimeMs

    { get; private set; }

    public long AllocatedMemorySize

    { get; private set; }

    public long SurvivedMemorySize

    { get; private set; }

    public override string ToString()

    {

        return string.Format(

            "AppDomain Friendly-name={0}, CPU={1}, \nAllocated MemorySize={2:N0}, Survived = {3:N0}",

            this.AppDomainFriendlyName,

            this.ProcessorTimeMs,

            this.AllocatedMemorySize,

            this.SurvivedMemorySize

            );

    }

}

Aqui sim, há umas coisinhas que quero comentar:

1. Optei por usar struct por entender que esse tipo é apenas uma representação de dados;

2. Deixei todo o tipo somente leitura;

3. Lanço uma exception no construtor por entender que não há sentido em aceitar a construção para esse tipo sem uma instância de AppDomainMonitor (tipo que estamos trabalhando);

4. Chamo, explicitamente, GC.Collect() para forçar o Gabage Collector a entrar em ação (para mim, esse é um método que não deve ser “evocado” sem uma ótima razão);

5. Criei uma versão mais amigável do ToString() com um resuminho do que o tipo representa.

Utilizando nosso “monitor”

Bem, agora que já temos nosso monitor, podemos testar o mesmo em nosso “código problema”. Veja:

var m = new AppDomainMonitor();

// alocando objetos em memória que não morrem;

var list = new List<Object>();

for (int i = 0; i < 512; i++)

    list.Add(new byte[1024]);

// objetos criados que morrem logo depois

for (int i = 0; i < 10000; i++)

    (new byte[1024]).GetType();

// parando 2 segundos

int stop = Environment.TickCount + 5000;

while (Environment.TickCount < stop) ;

Console.WriteLine(m.TakeSnapshot());

Repare que o custo de setup é quase nulo. Não informei o appdomain e utilizei o método TakeSnapshot() para criar um report do desempenho desse “bloquinho inútil de código”.

Porém…

Deixando mais explícito o bloco de código que está sendo testado

O problema que vejo no código anterior é que não há um indicativo código do bloco de código que está sendo “monitorado”. O que vou fazer para contornar esse “problema”. Bem, em primeiro lugar, vou escrever uma classe helper para meu monitor. Observe o código:

public class AppDomainMonitorDelta : IDisposable

{

    AppDomainMonitor MonitorField;

    public AppDomainMonitorDelta

        (AppDomain targetAppDomain = null)

    {

        this.MonitorField = new

            AppDomainMonitor(targetAppDomain);

    }

    public void Dispose()

    {

        Console.WriteLine(this.MonitorField.TakeSnapshot());

        this.MonitorField.Reset();

    }

}

Repare que usei implementei IDisposable. Estou usando um recurso não gerenciado? Não…. Então, para quê? Para poder escrever um código assim:

using (var monitor = new AppDomainMonitorDelta())

{

    // alocando objetos em memória que não morrem;

    var list = new List<Object>();

    for (int i = 0; i < 512; i++)

        list.Add(new byte[1024]);

    // objetos criados que morrem logo depois

    for (int i = 0; i < 10000; i++)

        (new byte[1024]).GetType();

    // parando 2 segundos

    int stop = Environment.TickCount + 5000;

    while (Environment.TickCount < stop) ;

}

Bem melhor!

Bem, por hoje, era isso Descrição: Smiley piscando

Elemar Rodrigues Severo Junior

Elemar Rodrigues Severo Junior - Gerente de Pesquisa e Desenvolvimento da Procad (www.procad.net)
Twitter.com/elemarjr