Très haute utilisation de la mémoire dans .NET 4.0

J’ai un service Windows C # que j’ai récemment transféré de .NET 3.5 à .NET 4.0. Aucun autre changement de code n’a été effectué.

Sur 3.5, l’utilisation de la mémoire pour une charge de travail donnée était d’environ 1,5 Go de mémoire et le débit était de 20 X par seconde. (Le X n’a ​​pas d’importance dans le contexte de cette question.)

Le même service exécuté sur 4.0 utilise entre 3 Go et 5 Go de mémoire et obtient moins de 4 fois par seconde. En fait, le service finira généralement par s’éteindre à mesure que l’utilisation de la mémoire continuera d’augmenter jusqu’à ce que mon système soit à 99% d’utilisation et que l’échange de fichiers de pages devienne fou.

Je ne sais pas si cela a à voir avec la collecte des ordures, ou quoi, mais j’ai du mal à le comprendre. Mon service de fenêtre utilise le GC “Serveur” via le commutateur de fichier de configuration ci-dessous:

   

Changer cette option en faux ne semble pas faire de différence. De plus, à partir de la lecture que j’ai faite sur le nouveau GC en 4.0, les gros changements n’affectent que le mode GC du poste de travail, pas le mode GC du serveur. Donc peut-être que GC n’a rien à voir avec le problème.

Des idées?

Eh bien, c’était intéressant.

La cause première s’avère être une modification du comportement de la classe LocalReport de SQL Server Reporting Services (v2010) lors de son exécution par dessus .NET 4.0.

Fondamentalement, Microsoft a modifié le comportement du traitement RDLC de sorte que chaque fois qu’un rapport était traité, il le faisait dans un domaine d’application distinct. Cela a été fait spécifiquement pour résoudre une fuite de mémoire causée par l’impossibilité de décharger des assemblys à partir de domaines d’application. Lorsque la classe LocalReport a traité un fichier RDLC, elle crée en réalité un assembly à la volée et le charge dans le domaine de l’application.

Dans mon cas, en raison du volume important de rapports que je traitais, cela entraînait la création d’un très grand nombre d’objects System.Runtime.Remoting.ServerIdentity. Ce fut mon conseil à la cause, car je ne comprenais pas pourquoi le traitement d’un RLDC nécessitait une communication à distance.

Bien sûr, pour appeler une méthode sur une classe dans un autre domaine d’application, la communication à distance est exactement ce que vous utilisez. Dans .NET 3.5, cela n’était pas nécessaire car, par défaut, l’assemblage RDLC était chargé dans le même domaine d’application. Dans .NET 4.0, cependant, un nouveau domaine d’application est créé par défaut.

Le correctif était assez facile. Avant tout, je devais activer la stratégie de sécurité héritée en utilisant la configuration suivante:

     

Ensuite, je devais forcer le traitement des RDLC dans le même domaine d’application que mon service en appelant les éléments suivants:

 myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence); 

Cela a résolu le problème.

Je suis tombé sur ce problème exact. Et il est vrai que les domaines d’application sont créés et non nettoyés. Cependant, je ne recommanderais pas de revenir à l’inheritance. Ils peuvent être nettoyés par ReleaseSandboxAppDomain ().

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

Certaines autres choses que je fais aussi pour nettoyer:

Se désabonner de tout événement SubreportProcessing, effacer les sources de données, supprimer le rapport.

Notre service Windows traite plusieurs rapports par seconde et il n’y a pas de fuites.

Tu pourrais vouloir

  • profiler le tas
  • utiliser WinDbg + SOS.dll pour déterminer quelle ressource est en train de fuir et d’où provient la référence

Peut-être certaines API ont-elles changé la sémantique ou il pourrait même y avoir un bogue dans la version 4.0 du framework

Pour être complet, si quelqu’un cherche le paramètre ASP.Net web.config équivalent, c’est:

     

ExecuteReportInCurrentAppDomain fonctionne de la même manière.

Merci à cette référence sociale MSDN .

Il semble que Microsoft ait essayé de placer le rapport dans son propre espace mémoire pour contourner toutes les memory leaks plutôt que de les réparer. Ce faisant, ils ont introduit des pannes et ont fini par avoir plus de memory leaks. Ils semblent mettre la définition de rapport en cache, mais ne l’utilisent jamais et ne la nettoient jamais. Chaque nouveau rapport crée une nouvelle définition de rapport, qui prend de plus en plus de mémoire.

J’ai joué avec la même chose: utiliser un domaine d’application séparé et y transférer le rapport. Je pense que c’est une solution terrible et fait très vite le bordel.

Ce que j’ai fait au lieu de cela est similaire: séparez la partie rapport de votre programme dans son propre programme de rapports. Cela s’avère être un bon moyen d’organiser votre code de toute façon.

La partie délicate consiste à transmettre des informations au programme séparé. Utilisez la classe Process pour démarrer une nouvelle instance du programme de rapports et transmettre les parameters nécessaires sur la ligne de commande. Le premier paramètre doit être une valeur enum ou similaire indiquant le rapport à imprimer. Mon code pour cela dans le programme principal ressemble à ceci:

 const ssortingng 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 ssortingng.Join RunWithArgs(pArgs.Select(arg => arg.ToSsortingng()).Join(" ")); } static void RunWithArgs(ssortingng pArgs) { Console.WriteLine("Running Report Program: {0} {1}", sReportsProgram, pArgs); var process = new Process(); process.StartInfo.FileName = sReportsProgram; process.StartInfo.Arguments = pArgs; process.Start(); } 

Et le programme de rapports ressemble à quelque chose comme:

 [STAThread] static void Main(ssortingng[] 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, ssortingng[] pArgs) { switch (pReportType) { case ReportType.Report1: return GetReport1Form(pArgs); case ReportType.Report2: return GetReport2Form(pArgs); default: throw new ArgumentOutOfRangeException("pReportType", pReportType, null); } } 

Vos méthodes GetReportForm doivent extraire la définition du rapport, utiliser des arguments pertinents pour obtenir le jeu de données, transmettre les données et tout autre argument au rapport, puis placer le rapport dans un visualiseur de rapports sur un formulaire et renvoyer une référence au formulaire. Notez qu’il est possible d’extraire une grande partie de ce processus afin que vous puissiez dire en gros «donnez-moi un formulaire pour ce rapport à partir de cet assembly en utilisant ces données et ces arguments».

Notez également que les deux programmes doivent être en mesure de voir vos types de données pertinents pour ce projet. J’espère donc que vous avez extrait vos classes de données dans leur propre bibliothèque, à laquelle ces deux programmes peuvent partager une référence. Il ne fonctionnerait pas d’avoir toutes les classes de données dans le programme principal, car vous auriez une dépendance circulaire entre le programme principal et le programme de rapport.

Ne le faites pas trop avec les arguments non plus. Faire des requêtes de firebase database dont vous avez besoin dans le programme de rapports; ne passez pas une énorme liste d’objects (qui ne fonctionneraient probablement pas de toute façon). Vous devriez simplement passer des choses simples comme les champs d’ID de firebase database, les plages de dates, etc.

Vous pouvez également placer une référence au programme de rapports dans votre programme principal, et le fichier .exe résultant et tous les fichiers .dll associés seront copiés dans le même dossier de sortie. Vous pouvez alors l’exécuter sans spécifier de chemin et utiliser simplement le nom du fichier exécutable (ex: “SomethingReports.exe”). Vous pouvez également supprimer les DLL de rapport du programme principal.

Un problème avec ceci est que vous obtiendrez une erreur manifeste si vous n’avez jamais publié le programme de rapports. Il suffit de le publier une fois pour générer un manifeste et cela fonctionnera.

Une fois que vous avez ce travail, il est très agréable de voir la mémoire de votre programme régulier restr constante lors de l’impression d’un rapport. Le programme de rapports apparaît, prenant plus de mémoire que votre programme principal, puis disparaît, le nettoyant complètement avec votre programme principal ne prenant plus de mémoire.

Un autre problème pourrait être que chaque instance de rapport prendra désormais plus de mémoire qu’auparavant, car il s’agit désormais de programmes distincts. Si l’utilisateur imprime beaucoup de rapports et ne les ferme jamais, il utilisera beaucoup de mémoire très rapidement. Mais je pense que c’est encore beaucoup mieux puisque cette mémoire peut facilement être récupérée simplement en fermant les rapports.

Cela rend également vos rapports indépendants de votre programme principal. Ils peuvent restr ouverts même après la fermeture du programme principal et vous pouvez les générer manuellement à partir de la ligne de commande ou d’autres sources.