Abrir o documento XPS no .NET causa um vazamento de memória
-
03-07-2019 - |
Pergunta
O snippet de código a seguir ilustra um vazamento de memória ao abrir arquivos XPS. Se você executá -lo e assistir ao gerenciador de tarefas, ele crescerá e não liberará memória até que o aplicativo saia.
******** Começa o aplicativo de console.
Module Main
Const DefaultTestFilePath As String = "D:\Test.xps"
Const DefaultLoopRuns As Integer = 1000
Public Sub Main(ByVal Args As String())
Dim PathToTestXps As String = DefaultTestFilePath
Dim NumberOfLoops As Integer = DefaultLoopRuns
If (Args.Count >= 1) Then PathToTestXps = Args(0)
If (Args.Count >= 2) Then NumberOfLoops = CInt(Args(1))
Console.Clear()
Console.WriteLine("Start - {0}", GC.GetTotalMemory(True))
For LoopCount As Integer = 1 To NumberOfLoops
Console.CursorLeft = 0
Console.Write("Loop {0:d5}", LoopCount)
' The more complex the XPS document and the more loops, the more memory is lost.
Using XPSItem As New Windows.Xps.Packaging.XpsDocument(PathToTestXps, System.IO.FileAccess.Read)
Dim FixedDocSequence As Windows.Documents.FixedDocumentSequence
' This line leaks a chunk of memory each time, when commented out it does not.
FixedDocSequence = XPSItem.GetFixedDocumentSequence
End Using
Next
Console.WriteLine()
GC.Collect() ' This line has no effect, I think the memory that has leaked is unmanaged (C++ XPS internals).
Console.WriteLine("Complete - {0}", GC.GetTotalMemory(True))
Console.WriteLine("Loop complete but memory not released, will release when app exits (press a key to exit).")
Console.ReadKey()
End Sub
End Module
'****** O aplicativo de console termina.
A razão pela qual ele faz milhares de vezes é porque meu código processa muitos arquivos e vazamentos de memória forçando rapidamente uma O outOfMemoryException. Forçar a coleta de lixo não funciona (suspeito que seja um pedaço de memória não gerenciado nos internos do XPS).
O código estava originalmente em outro tópico e classe, mas foi simplificado para isso.
Qualquer ajuda muito apreciada.
Ryan
Solução
Bem, eu encontrei. É um bug na estrutura e, para contorná -lo, você adiciona uma chamada para o UpdateLayout. O uso da instrução pode ser alterado para o seguinte para fornecer uma correção;
Using XPSItem As New Windows.Xps.Packaging.XpsDocument(PathToTestXps, System.IO.FileAccess.Read)
Dim FixedDocSequence As Windows.Documents.FixedDocumentSequence
Dim DocPager As Windows.Documents.DocumentPaginator
FixedDocSequence = XPSItem.GetFixedDocumentSequence
DocPager = FixedDocSequence.DocumentPaginator
DocPager.ComputePageCount()
' This is the fix, each page must be laid out otherwise resources are never released.'
For PageIndex As Integer = 0 To DocPager.PageCount - 1
DirectCast(DocPager.GetPage(PageIndex).Visual, Windows.Documents.FixedPage).UpdateLayout()
Next
FixedDocSequence = Nothing
End Using
Outras dicas
Encontrou isso hoje. Curiosamente, quando eu olhei para as coisas usando o refletor.net, encontrei a correção envolvia a chamada updateLayout () no contextLayoutManager associado ao despachante atual. (Leia: Não há necessidade de iterar nas páginas).
Basicamente, o código a ser chamado (use reflexão aqui) é:
ContextLayoutManager.From(Dispatcher.CurrentDispatcher).UpdateLayout();
Definitivamente parece uma pequena supervisão de MS.
Para os preguiçosos ou desconhecidos, esse código funciona:
Assembly presentationCoreAssembly = Assembly.GetAssembly(typeof (System.Windows.UIElement));
Type contextLayoutManagerType = presentationCoreAssembly.GetType("System.Windows.ContextLayoutManager");
object contextLayoutManager = contextLayoutManagerType.InvokeMember("From",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, new[] {dispatcher});
contextLayoutManagerType.InvokeMember("UpdateLayout", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, contextLayoutManager, null);
O FXCOP reclamará, mas talvez seja corrigido na próxima versão da estrutura. O código publicado pelo autor parece ser "mais seguro" se você preferir não usar a reflexão.
HTH!
Não posso te dar nenhum conselho de autoridade, mas tive alguns pensamentos:
- Se você quiser assistir sua memória dentro do loop, também precisa coletar memória dentro do loop. Caso contrário, você irá aparecer vazar memória por design, pois é mais eficiente coletar blocos maiores com menos frequência (conforme necessário), em vez de estar constantemente coletando pequenas quantidades. Nesse caso, o bloco de escopo criando a instrução usando deve Seja suficiente, mas o uso do GC.Collect indica que talvez algo mais esteja acontecendo.
- Até o gc.collect é apenas uma sugestão (ok, muito Forte sugestão, mas ainda uma sugestão): não garante que toda a memória pendente seja coletada.
- Se o código XPS interno realmente estiver vazando memória, a única maneira de forçar o sistema operacional a cobrá -lo é enganar o sistema operacional a pensar que o aplicativo terminou. Para fazer isso, você talvez possa criar um aplicativo fictício que lida com seu código XPS e seja chamado do aplicativo principal, ou mover o código XPS para o seu próprio aplicativo dentro do seu código principal também pode ser suficiente.
Adicionar updatelayout não pode resolver o problema. De acordo com http://support.microsoft.com/kb/942443, "pré -carregue o arquivo ApresentationCore.dll ou o arquivo ApresentationFramework.dll no domínio do aplicativo primário" é necessário.
Interessante. O problema ainda está presente no .NET Framework 4.0. Meu código estava vazando ferozmente.
A correção proposta - onde o UpdateLayout é chamado em um loop imediatamente após a criação do FixedDocumentSequence não corrigiu o problema para mim em um documento de teste de 400 páginas.
No entanto, a solução a seguir resolveu o problema para mim. Como nas correções anteriores, mudei a chamada para getFixedDocumentSequence () fora do loop de cada página. A cláusula "Usando" ... Aviso justo de que ainda não tenho certeza de que está correto. Mas não está doendo. O documento é posteriormente reutilizado para gerar visualizações de página na tela. Portanto, não parece doer.
DocumentPaginator paginator
= document.GetFixedDocumentSequence().DocumentPaginator;
int numberOfPages = paginator.ComputePageCount();
for (int i = 0; i < NumberOfPages; ++i)
{
DocumentPage docPage = paginator.GetPage(nPage);
using (docPage) // using is *probably* correct.
{
// VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
((FixedPage)(docPage.Visual)).UpdateLayout();
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Adding THAT line cured my leak.
RenderTargetBitmap bitmap = GetXpsPageAsBitmap(docPage, dpi);
.... etc...
}
}
Na realidade, a linha de fixação entra na minha rotina getXPSPAGeasBitMap (Omited for Clarity), que é praticamente idêntica ao código publicado anteriormente.
Obrigado a todos que contribuíram.