Uso de memory muito alta no .NET 4.0

Eu tenho um serviço do Windows em C # que eu mudei recentemente do .NET 3.5 para o .NET 4.0. Nenhuma outra alteração de código foi feita.

Quando rodando em 3.5, o uso da memory para uma determinada carga de trabalho era de aproximadamente 1,5 GB de memory e a taxa de transferência era de 20 X por segundo. (O X não importa no contexto desta questão.)

O mesmo serviço em execução no 4.0 usa entre 3 GB e 5 GB + de memory e obtém menos de 4 X por segundo. Na verdade, o serviço normalmente acabará se esgotando à medida que o uso da memory continua a subir até que o meu sistema esteja localizado a uma taxa de utilização de 99% e a troca de arquivos de paginação enlouqueça.

Eu não tenho certeza se isso tem a ver com a garbage collection, ou o que, mas eu estou tendo problemas para descobrir isso. Meu serviço de janela usa o GC “Server” através do switch de arquivo de configuração visto abaixo:

   

Mudar esta opção para falso não parece fazer diferença. Além disso, a partir da leitura que fiz no novo GC em 4.0, as grandes alterações afetam apenas o modo GC da estação de trabalho, não o modo GC do servidor. Então talvez GC não tenha nada a ver com o problema.

Idéias?

Bem, isso foi interessante.

A causa raiz acaba sendo uma alteração no comportamento da class LocalReport (v2010) do SQL Server Reporting Services ao executar isso em cima do .NET 4.0.

Basicamente, a Microsoft alterou o comportamento do processamento de RDLC para que cada vez que um relatório fosse processado fosse feito em um domínio de aplicativo separado. Isso foi realmente feito especificamente para resolver um memory leaks causado pela incapacidade de descarregar assemblies de domínios de aplicativo. Quando a class LocalReport processou um arquivo RDLC, ele realmente cria um assembly em tempo real e o carrega no domínio do aplicativo.

No meu caso, devido ao grande volume de relatórios que eu estava processando, isso resultava em números muito grandes de objects System.Runtime.Remoting.ServerIdentity sendo criados. Esta foi a minha dica para a causa, pois fiquei confuso sobre o motivo pelo qual o processamento de um RLDC requeria o remoting.

Claro, para chamar um método em uma class em outro domínio de aplicativo, o remoting é exatamente o que você usa. No .NET 3.5, isso não era necessário, pois, por padrão, o assembly RDLC era carregado no mesmo domínio do aplicativo. No .NET 4.0, no entanto, um novo domínio de aplicativo é criado por padrão.

A correção foi bastante fácil. Primeiro eu precisava ativar a política de segurança legada usando a seguinte configuração:

     

Em seguida, precisei forçar os RDLCs a serem processados ​​no mesmo domínio de aplicativo do meu serviço, chamando o seguinte:

 myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence); 

Isso resolveu o problema.

Eu me deparei com esse problema exato. E é verdade que os domínios de aplicativos são criados e não são limpos. No entanto, eu não recomendaria reverter para o legado. Eles podem ser limpos pelo ReleaseSandboxAppDomain ().

 LocalReport report = new LocalReport(); ... report.ReleaseSandboxAppDomain(); 

Algumas outras coisas que eu também faço para limpar:

Cancelar a assinatura de qualquer evento de sub-relatório de processamento, limpar fonts de dados, descartar o relatório.

Nosso serviço do Windows processa vários relatórios por segundo e não há vazamentos.

Você pode querer

  • perfil do heap
  • use WinDbg + SOS.dll para estabelecer qual recurso está sendo vazado e de onde a referência é mantida

Talvez alguma API tenha mudado a semântica ou pode até haver um bug na versão 4.0 do framework

Apenas para completar, se alguém estiver procurando pela configuração equivalente do ASP.Net web.config , é:

     

ExecuteReportInCurrentAppDomain funciona da mesma maneira.

Graças a esta referência do Social MSDN .

Parece que a Microsoft tentou colocar o relatório em seu próprio espaço de memory separado para contornar todos os vazamentos de memory em vez de corrigi-los. Ao fazer isso, eles introduziram algumas falhas graves e acabaram tendo mais vazamentos de memory. Eles parecem armazenar em cache a definição do relatório, mas nunca usá-lo e nunca limpá-lo, e cada novo relatório cria uma nova definição de relatório, ocupando cada vez mais memory.

Brinquei fazendo a mesma coisa: usar um domínio de aplicativo separado e enviá-lo para o relatório. Eu acho que é uma solução terrível e faz uma bagunça muito rapidamente.

Em vez disso, o que fiz foi semelhante: dividir a parte do relatório do seu programa em seu próprio programa de relatórios separado. Isso acaba sendo uma boa maneira de organizar seu código de qualquer maneira.

A parte complicada é passar informações para o programa separado. Use a class Process para iniciar uma nova instância do programa de relatórios e passar quaisquer parâmetros necessários na linha de comando. O primeiro parâmetro deve ser um enum ou valor semelhante, indicando o relatório que deve ser impresso. Meu código para isso no programa principal é algo como:

 const string sReportsProgram = "SomethingReports.exe"; public static void RunReport1(DateTime pDate, int pSomeID, int pSomeOtherID) { RunWithArgs(ReportType.Report1, pDate, pSomeID, pSomeOtherID); } public static void RunReport2(int pSomeID) { RunWithArgs(ReportType.Report2, pSomeID); } // TODO: currently no support for quoted args static void RunWithArgs(params object[] pArgs) { // .Join here is my own extension method which calls string.Join RunWithArgs(pArgs.Select(arg => arg.ToString()).Join(" ")); } static void RunWithArgs(string pArgs) { Console.WriteLine("Running Report Program: {0} {1}", sReportsProgram, pArgs); var process = new Process(); process.StartInfo.FileName = sReportsProgram; process.StartInfo.Arguments = pArgs; process.Start(); } 

E o programa de relatórios parece algo como:

 [STAThread] static void Main(string[] pArgs) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); var reportType = (ReportType)Enum.Parse(typeof(ReportType), pArgs[0]); using (var reportForm = GetReportForm(reportType, pArgs)) Application.Run(reportForm); } static Form GetReportForm(ReportType pReportType, string[] pArgs) { switch (pReportType) { case ReportType.Report1: return GetReport1Form(pArgs); case ReportType.Report2: return GetReport2Form(pArgs); default: throw new ArgumentOutOfRangeException("pReportType", pReportType, null); } } 

Seus methods GetReportForm devem extrair a definição de relatório, usar argumentos relevantes para obter o dataset, passar os dados e quaisquer outros argumentos para o relatório e, em seguida, colocar o relatório em um visualizador de relatório em um formulário e retornar uma referência ao formulário. Note que é possível extrair grande parte deste processo para que você possa basicamente dizer ‘me dê um formulário para este relatório deste assembly usando estes dados e esses argumentos’.

Observe também que ambos os programas devem ser capazes de ver seus tipos de dados que são relevantes para este projeto, portanto, esperamos que você tenha extraído suas classs de dados em sua própria biblioteca, à qual ambos os programas podem compartilhar uma referência. Não funcionaria ter todas as classs de dados no programa principal, porque você teria uma dependência circular entre o programa principal e o programa de relatório.

Não faça mais com os argumentos, também. Faça qualquer consulta de database que você precise no programa de relatórios; não passe uma lista enorme de objects (o que provavelmente não funcionaria de qualquer maneira). Você deve apenas passar informações simples como campos de ID do database, intervalos de datas, etc. Se você tiver parâmetros particularmente complexos, pode ser necessário enviar essa parte da interface do usuário para o programa de relatórios e não passá-los como argumentos na linha de comando.

Você também pode colocar uma referência ao programa de relatórios em seu programa principal, e o .exe resultante e qualquer .dlls relacionado serão copiados para a mesma pasta de saída. Você pode, então, executá-lo sem especificar um caminho e apenas usar o nome do arquivo executável por si só (por exemplo: “SomethingReports.exe”). Você também pode remover as dlls de relatório do programa principal.

Um problema com isso é que você receberá um erro de manifesto se você nunca publicou o programa de relatórios. Apenas dummy publicar uma vez, para gerar um manifesto e, em seguida, vai funcionar.

Depois de ter este trabalho, é muito bom ver a memory do seu programa regular permanecer constante ao imprimir um relatório. O programa de relatórios aparece, ocupando mais memory do que o seu programa principal, e depois desaparece, limpando-o completamente com o seu programa principal ocupando mais memory do que ele já tinha.

Outro problema pode ser que cada instância de relatório agora ocupará mais memory do que antes, já que agora são programas separados inteiros. Se o usuário imprimir muitos relatórios e nunca os fechar, isso consumirá muita memory muito rapidamente. Mas acho que isso ainda é muito melhor, já que a memory pode ser facilmente recuperada simplesmente fechando os relatórios.

Isso também torna seus relatórios independentes do seu programa principal. Eles podem permanecer abertos mesmo depois de fechar o programa principal, e você pode gerá-los a partir da linha de comando manualmente, ou de outras fonts também.