Abrir un documento XPS en .Net provoca una pérdida de memoria
-
03-07-2019 - |
Pregunta
El siguiente fragmento de código ilustra una pérdida de memoria al abrir archivos XPS. Si lo ejecuta y mira el administrador de tareas, crecerá y no liberará memoria hasta que la aplicación se cierre.
'****** COMIENZA la aplicación de consola.
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
'****** La aplicación de consola termina.
La razón por la que se repite mil veces es porque mi código procesa muchos archivos y pierde memoria rápidamente forzando una excepción OutOfMemoryException. Forzar la recolección de basura no funciona (sospecho que es una porción de memoria no administrada en las partes internas de XPS).
El código estaba originalmente en otro hilo y clase pero se ha simplificado a esto.
Cualquier ayuda muy apreciada.
Ryan
Solución
Bueno, lo encontré. ES un error en el marco y para solucionarlo, agregue una llamada a UpdateLayout. El uso de la declaración se puede cambiar a lo siguiente para proporcionar una solución;
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
Otros consejos
Me encontré con esto hoy. Curiosamente, cuando miré las cosas usando Reflector.NET, encontré que la solución implicaba llamar a UpdateLayout () en el ContextLayoutManager asociado con el Dispatcher actual. (lea: no es necesario iterar sobre las páginas).
Básicamente, el código que se llamará (use la reflexión aquí) es:
ContextLayoutManager.From(Dispatcher.CurrentDispatcher).UpdateLayout();
Definitivamente se siente como un pequeño descuido por parte de MS.
Para los perezosos o desconocidos, este 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);
FxCop se quejará, pero tal vez esté solucionado en la próxima versión del framework. El código publicado por el autor parece ser "más seguro" si prefiere no usar la reflexión.
HTH!
No puedo darle ningún consejo autorizado, pero tuve algunas ideas:
- Si desea ver su memoria dentro del ciclo, también debe estar recolectando memoria dentro del ciclo. De lo contrario, parecerá perder memoria por diseño, ya que es más eficiente recolectar bloques más grandes con menos frecuencia (según sea necesario) en lugar de recolectar constantemente pequeñas cantidades. En este caso, el bloque de alcance que crea la declaración de uso debería ser suficiente, pero su uso de GC.Collect indica que tal vez algo más está sucediendo.
- Incluso GC.Collect es solo una sugerencia (bueno, muy fuerte sugerencia, pero sigue siendo una sugerencia): no garantiza que se recopile toda la memoria pendiente.
- Si el código XPS interno realmente está perdiendo memoria, la única forma de obligar al sistema operativo a recopilarlo es engañando al sistema operativo para que piense que la aplicación ha finalizado. Para hacer eso, tal vez podría crear una aplicación ficticia que maneje su código xps y se llame desde la aplicación principal, o mover el código xps a su propio AppDomain dentro de su código principal también puede ser suficiente.
Agregar UpdateLayout no puede resolver el problema. De acuerdo con http://support.microsoft.com/kb/942443 , " precargar el PresentationCore .dll o el archivo PresentationFramework.dll en el dominio de la aplicación principal " es necesario.
Interesante. El problema todavía está presente en .net framework 4.0. Mi código estaba goteando ferozmente.
La solución propuesta: donde UpdateLayout se llama en un bucle inmediatamente después de la creación de FixedDocumentSequence NO me solucionó el problema en un documento de prueba de 400 páginas.
Sin embargo, la siguiente solución me solucionó el problema. Como en arreglos anteriores, moví la llamada a GetFixedDocumentSequence () fuera del ciclo de cada página. El " utilizando " cláusula ... advertencia justa de que todavía no estoy seguro de que sea correcto. Pero no duele. El documento se reutiliza posteriormente para generar vistas previas de página en pantalla. Por lo tanto, no parece doler.
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...
}
}
En realidad, la línea de arreglo va dentro de mi rutina GetXpsPageAsBitmap (omitida por claridad), que es prácticamente idéntica al código publicado anteriormente.
Gracias a todos los que contribuyeron.