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 JuniorOlá 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!
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;
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 . 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