L'ouverture d'un document XPS en .Net provoque une fuite de mémoire
-
03-07-2019 - |
Question
L'extrait de code suivant illustre une fuite de mémoire lors de l'ouverture de fichiers XPS. Si vous l'exécutez et surveillez le gestionnaire de tâches, il s'agrandira et ne libérera pas de mémoire jusqu'à la fermeture de l'application.
'****** L'application de la console COMMENCE.
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
'****** FIN de l'application console.
La raison pour laquelle elle effectue des boucles mille fois est que mon code traite beaucoup de fichiers et perd de la mémoire rapidement, forçant une exception OutOfMemoryException. Forcer le ramassage des ordures ne fonctionne pas (je suppose que c'est un bloc de mémoire non géré dans les composants internes XPS).
Le code était à l'origine dans un autre thread et une autre classe, mais a été simplifié.
Toute aide grandement appréciée.
Ryan
La solution
Eh bien, je l'ai trouvé. C’est un bogue dans le framework et pour le contourner, vous ajoutez un appel à UpdateLayout. L’utilisation de statement peut être modifiée comme suit pour fournir un correctif;
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
Autres conseils
Ran dans cette aujourd'hui. Il est intéressant de noter que lorsque je me suis intéressé à Reflector.NET, j’ai trouvé que le correctif impliquait d’appeler UpdateLayout () sur le ContextLayoutManager associé au Dispatcher actuel. (lire: inutile de parcourir les pages).
En gros, le code à appeler (utilisez la réflexion ici) est:
ContextLayoutManager.From(Dispatcher.CurrentDispatcher).UpdateLayout();
Cela ressemble vraiment à un petit oubli de la part de MS.
Pour les paresseux ou les inconnus, ce code fonctionne:
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 va se plaindre, mais c'est peut-être corrigé dans la prochaine version du framework. Le code publié par l'auteur semble être "plus sûr". si vous préférez ne pas utiliser la réflexion.
HTH!
Je ne peux vous donner aucun conseil faisant autorité, mais j’ai eu quelques réflexions:
- Si vous souhaitez surveiller votre mémoire dans la boucle, vous devez également la collecter. Sinon, vous aurez l'impression de perdre de la mémoire, car il est plus efficace de collecter des blocs plus volumineux moins souvent (au besoin) plutôt que de collecter constamment de petites quantités. Dans ce cas, le bloc de portée créant l’instruction using devrait suffire, mais votre utilisation de GC.Collect indique qu’il se peut que quelque chose d’autre se passe.
- Même GC.Collect n'est qu'une suggestion (d'accord, suggestion très forte , mais toujours une suggestion): cela ne garantit pas que toute la mémoire en suspens est collectée.
- Si le code XPS interne contient réellement une fuite de mémoire, le seul moyen de forcer le système d'exploitation à le collecter est de le faire croire à la fin de l'application. Pour ce faire, vous pourriez peut-être créer une application factice qui gère votre code xps et qui est appelée à partir de l'application principale, ou déplacer le code xps dans son propre AppDomain dans votre code principal peut également suffire.
Ajouter UpdateLayout ne peut pas résoudre le problème. Selon http://support.microsoft.com/kb/942443 , préchargez le fichier PresentationCore. Fichier .dll ou PresentationFramework.dll du domaine d’application principal " est nécessaire.
Intéressant. Le problème est toujours présent dans .net Framework 4.0. Mon code fuyait férocement.
Le correctif proposé - où UpdateLayout est appelé dans une boucle immédiatement après la création de FixedDocumentSequence NE résout PAS le problème pour moi sur un document de test de 400 pages.
Cependant, la solution suivante a résolu le problème pour moi. Comme dans les correctifs précédents, j'ai déplacé l'appel à GetFixedDocumentSequence () en dehors de la boucle for-each-page. Le " using " clause ... avertissement juste que je ne suis toujours pas sûr que ce soit correct. Mais ça ne fait pas mal. Le document est ensuite réutilisé pour générer des aperçus de page à l'écran. Donc, cela ne semble pas faire mal.
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 réalité, la ligne de correction va dans ma routine GetXpsPageAsBitmap (ommited pour plus de clarté), ce qui est pratiquement identique au code précédemment posté.
Merci à tous ceux qui ont contribué.