web-dev-qa-db-fra.com

Utilisation très élevée de la mémoire dans .NET 4.0

J'ai un service Windows C # que j'ai récemment déplacé de .NET 3.5 vers .NET 4.0. Aucune autre modification de code n'a été apportée.

Lors de l'exécution 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 fonctionnant sur 4.0 utilise entre 3 Go et 5 Go + de mémoire et obtient moins de 4 X par seconde. En fait, le service finira généralement par se bloquer à mesure que l'utilisation de la mémoire continuera d'augmenter jusqu'à ce que mon système soit à 99% et que l'échange de fichiers d'échange 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:

  <runtime>
    <gcServer enabled="true"/>
  </runtime>

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

Des idées?

63
RMD

Eh bien, c'était intéressant.

La cause première se révèle être un changement dans le comportement de la classe LocalReport de SQL Server Reporting Services (v2010) lors de son exécution au-dessus de .NET 4.0.

Fondamentalement, Microsoft a modifié le comportement du traitement RDLC afin que chaque fois qu'un rapport soit traité, il le soit 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 les assemblys des domaines d'application. Lorsque la classe LocalReport a traité un fichier RDLC, elle crée en fait un assembly à la volée et le charge dans le domaine d'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'objets System.Runtime.Remoting.ServerIdentity. C'était mon conseil à la cause, car je ne savais pas pourquoi le traitement d'un RLDC nécessitait un accès à 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.

La correction a été assez facile. J'ai d'abord dû activer la stratégie de sécurité héritée en utilisant la configuration suivante:

  <runtime>
    <NetFx40_LegacySecurityPolicy enabled="true"/>
  </runtime>

Ensuite, je devais forcer le traitement des RDLC dans le même domaine d'application que mon service en appelant ce qui suit:

myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence);

Cela a résolu le problème.

83
RMD

J'ai rencontré 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'héritage. Ils peuvent être nettoyés par ReleaseSandboxAppDomain ().

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

D'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 aucune fuite.

11
sues999

Tu pourrais vouloir

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

4
sehe

Juste pour être complet, si quelqu'un cherche l'équivalent ASP.Net web.config paramètre, c'est:

  <system.web>
    <trust legacyCasModel="true" level="Full"/>
  </system.web>

ExecuteReportInCurrentAppDomain fonctionne de la même manière.

Merci à cela référence MSDN sociale .

2
StuartLC

Il semble que Microsoft ait essayé de placer le rapport dans son propre espace mémoire séparé pour contourner toutes les fuites de mémoire plutôt que de les corriger. Ce faisant, ils ont introduit des plantages et ont fini par avoir plus de fuites de mémoire de toute façon. Ils semblent mettre en cache la définition de rapport, mais ne l'utilisent jamais et ne la nettoient jamais, et chaque nouveau rapport crée une nouvelle définition de rapport, occupant de plus en plus de mémoire.

J'ai joué avec la même chose: utiliser un domaine d'application distinct et rassembler le rapport dessus. Je pense que c'est une solution terrible et fait un gâchis très rapidement.

Ce que j'ai fait à la place est similaire: diviser la partie de rapport de votre programme en son propre programme de rapports séparé. Cela s'avère tout de même être un bon moyen d'organiser votre code.

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 passer tous les paramètres dont il a besoin sur la ligne de commande. Le premier paramètre doit être une énumération ou une valeur similaire indiquant le rapport à imprimer. Mon code pour cela dans le programme principal ressemble à ceci:

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();
}

Et le programme de rapports ressemble à ceci:

[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);
   }
}

Vos méthodes GetReportForm doivent extraire la définition du rapport, utiliser des arguments pertinents pour obtenir l'ensemble de données, transmettre les données et tout autre argument au rapport, puis placer le rapport dans une visionneuse de rapport sur un formulaire et renvoyer un 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 de cette Assemblée 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 qui sont pertinents pour ce projet, donc j'espère 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. Faites toute requête de base de données dont vous avez besoin dans le programme de rapports; ne passez pas une énorme liste d'objets (qui ne fonctionnerait probablement pas de toute façon). Vous devez simplement transmettre des éléments simples tels que des champs d'ID de base de données, des plages de dates, etc. Si vous avez des paramètres particulièrement complexes, vous devrez peut-être également pousser cette partie de l'interface utilisateur vers le programme de rapports et ne pas les passer comme arguments sur la ligne de commande.

Vous pouvez également mettre 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 ensuite l'exécuter sans spécifier de chemin d'accès et simplement utiliser le nom de fichier exécutable par lui-même (par exemple: "SomethingReports.exe"). Vous pouvez également supprimer les DLL de rapport du programme principal.

Un problème avec cela 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, puis cela fonctionnera.

Une fois que cela fonctionne, il est très agréable de voir la mémoire de votre programme régulier rester 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 n'occupant pas plus de mémoire qu'il n'en avait déjà.

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 un grand nombre de rapports et ne les ferme jamais, il utilisera beaucoup de mémoire très rapidement. Mais je pense que c'est encore mieux car 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 rester 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 à partir d'autres sources.

1
Dave Cousineau

Je suis assez en retard, mais j'ai une vraie solution et je peux expliquer pourquoi!

Il s'avère que LocalReport utilise ici .NET Remoting pour créer dynamiquement un sous-domaine d'application et exécuter le rapport afin d'éviter une fuite en interne quelque part. On remarque alors que, finalement, le rapport va libérer toute la mémoire après 10 à 20 minutes. Pour les personnes avec beaucoup de PDF générés, cela ne fonctionnera pas. Cependant, la clé ici est qu'ils utilisent .NET Remoting. L'un des éléments clés du Remoting est ce que l'on appelle le "Leasing". La location signifie qu'elle gardera cet objet de maréchal autour pendant un certain temps, car la connexion à distance est généralement coûteuse à configurer et va probablement être utilisée plusieurs fois. LocalReport RDLC en abuse.

Par défaut, la durée du leasing est de ... 10 minutes! De plus, si quelque chose fait divers appels, cela ajoute encore 2 minutes au temps d'attente! Ainsi, cela peut être aléatoire entre 10 et 20 minutes selon la façon dont les appels s'alignent. Heureusement, vous pouvez modifier la durée de ce délai. Malheureusement, vous ne pouvez le définir qu'une seule fois par domaine d'application ... Ainsi, si vous avez besoin d'un accès à distance autre que PDF, vous devrez probablement faire en sorte qu'un autre service l'exécute afin que vous puissiez modifier les paramètres par défaut Pour ce faire, il vous suffit d'exécuter ces 4 lignes de code au démarrage:

    LifetimeServices.LeaseTime = TimeSpan.FromSeconds(5);
    LifetimeServices.LeaseManagerPollTime = TimeSpan.FromSeconds(5);
    LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(1);
    LifetimeServices.SponsorshipTimeout = TimeSpan.FromSeconds(5);

Vous verrez que l'utilisation de la mémoire commence à augmenter, puis dans quelques secondes, vous devriez voir la mémoire commencer à redescendre. Cela m'a pris des jours avec un profileur de mémoire pour vraiment suivre cela et réaliser ce qui se passait.

Vous ne pouvez pas envelopper ReportViewer dans une instruction using (Dispose crashes), mais vous devriez pouvoir le faire si vous utilisez LocalReport directement. Après cela, vous pouvez appeler GC.Collect () si vous voulez être doublement sûr de faire tout ce que vous pouvez pour libérer cette mémoire.

J'espère que cela t'aides!

Modifier

Apparemment, vous devriez appeler GC.Collect (0) après avoir généré un rapport PDF PDF, sinon il semble que l'utilisation de la mémoire puisse encore être élevée pour une raison quelconque.

0
Daniel Lorenz