[reportlab-users] Re: Generating huge reports and memory consumption
François Pinard
reportlab-users@reportlab.com
03 Jan 2003 10:10:55 -0500
--=-=-=
[Alessandro Praduroux]
> The problem I have is that the memory usage goes up [...] I can't
> determine exactly which object and where it's created. Any suggestion?
I was recently asked to solve a problem of a long running Python process
that was progressively consuming all memory. The documentation of
`gc.referrees' seemed quite promising for deeper searching, but we found the
problem before we really needed it, and moreover, as I see today, this does
not seem available for Python 2.2.1, which was the installed version. It
turned out that there was a leak problem in Python itself associated with
one new-style class (only 6 instances of that class in the whole program).
We removed the derivation of that class from `object', and all went fine.
The following tool, which I wrote for the circumstance, helped me at
spotting where the problem was. Not really the Gnuplot graphics, but the
delta traces on stderr. The comments are in French, I hope you will be able
to make some sense out of the thing nevertheless. If not, write to me! :-)
--=-=-=
Content-Type: text/plain; charset=iso-8859-1
Content-Disposition: attachment; filename=ressources.py
Content-Transfer-Encoding: 8bit
#!/usr/bin/env python
# Copyright © 2002 Progiciels Bourbeau-Pinard inc.
# François Pinard <pinard@iro.umontreal.ca>, décembre 2002.
"""\
Outils divers pour explorer l'utilisation dyamique des ressources par
un programme. Une attention particulière est donnée aux problèmes de
déperdition de mémoire, puisque c'est le besoin à l'origine de ce module.
"""
# La fonction qui suit doit être appelée à répétition durant l'exécution
# d'un programme, typiquement une fois par exécution de sa boucle
# principale. Elle fournit simultanément plusieurs résultats illustrant
# l'utilisation des ressources.
# Si la variable d'environnement DISPLAY est définie et que le programme
# Gnuplot existe le long du chemin de fouille, un graphique sera mis à jour
# quant à la quantité de mémoire centrale occupée, et le nombre d'objets
# Python que le programme utilise couramment. Le double graphique montre
# les 50 dernières valeurs accumulées.
# Si TITRE ou WRITE est fourni, un rapport détaillé est fourni sur le
# nombre d'objets Python qui ont été créés ou détruits depuis l'appel
# précédent. Après une ligne de titre, chaque ligne montre le nombre
# d'objets avant le changement, un signe donnant le sens du changement,
# et l'amplitude du changement pour la classe indiquée; les lignes sont
# triées pour présenter d'abord la plus grande augmentation et terminer
# par la plus grande diminution. Si TITRE n'est pas fourni, une ligne
# de tirets est utilisée. Si WRITE n'est pas fourni, le rapport sera
# produit sur l'erreur standard.
# Le nombre d'objets d'une classe donnée est estimé (en fait, légèrement
# surestimé) par le nombre de références à cette classe. Les classes,
# autant les traditionnelles que celles du nouveau style (les types), sont
# trouvées dans l'espace global de tous les modules couramment importés,
# et pas ailleurs. Donc, les classes dont la portée est imbriquée dans
# des fonctions ou d'autres classes sont ignorées.
affichage_minimum = 20 # nombre de points initialement
affichage_maximum = 50 # nombre de points qui glissent
def rapporter(titre=None, write=None,
cache=[]):
if not cache:
cache.append(Rapporteur())
cache[0].rapporter(titre, write)
class Rapporteur:
def __init__(self):
self.gnuplot = nouveau_gnuplot()
self.points_elimines = 0
self.references = []
self.memoire_cpu = []
self.references_par_classe = {}
def rapporter(self, titre, write):
avant = self.references_par_classe
apres = self.references_par_classe = {}
total = 0
import sys, types
for module in sys.modules.itervalues():
if type(module) is types.ModuleType:
for nom, valeur in module.__dict__.iteritems():
if type(valeur) in (types.ClassType, types.TypeType):
compteur = sys.getrefcount(valeur)
apres[module.__name__, nom] = compteur
total += compteur
if titre is not None or write is not None:
self.rapporter_differences(titre, write, avant, apres)
if self.gnuplot is not None:
memoire = int(file('/proc/self/statm').read().split()[0])
if len(self.references) == 0:
self.references_min = self.references_max = total
self.memoire_cpu_min = self.memoire_cpu_max = memoire
else:
self.references_min = min(self.references_min, total)
self.references_max = max(self.references_max, total)
self.memoire_cpu_min = min(self.memoire_cpu_min, memoire)
self.memoire_cpu_max = max(self.memoire_cpu_max, memoire)
if len(self.references) == affichage_maximum:
del self.references[0]
del self.memoire_cpu[0]
self.points_elimines += 1
if (self.points_elimines + affichage_maximum) % 510 == 0:
# Gnuplot semble bloquer au 512ième graphique. :-(
self.gnuplot.close()
self.gnuplot = nouveau_gnuplot()
if self.gnuplot is None:
return
self.references.append(total)
self.memoire_cpu.append(memoire)
self.afficher_via_gnuplot(self.gnuplot.write)
self.gnuplot.flush()
def rapporter_differences(self, titre, write, avant, apres):
if titre is None:
titre = '-' * 79
if write is None:
import sys
write = sys.stderr.write
write(titre + '\n')
pertes = []
for cle, auparavant in avant.iteritems():
if cle in apres:
perte = auparavant - apres[cle]
if perte != 0:
pertes.append((perte, cle, auparavant))
else:
pertes.append((auparavant, cle, auparavant))
for cle, maintenant in apres.iteritems():
if cle not in avant:
pertes.append((-maintenant, cle, 0))
pertes.sort()
for perte, (module, classe), auparavant in pertes:
if perte < 0:
write("%5d + %-5d références à %s.%s\n"
% (auparavant, -perte, module, classe))
else:
write("%5d - %-5d références à %s.%s\n"
% (auparavant, perte, module, classe))
def afficher_via_gnuplot(self, write):
def tracer(titre, data, minimum, maximum):
abcisse = self.points_elimines
limite = max(affichage_minimum, abcisse + len(data))
write('plot [%d:%d] [%d:%d] \'-\' title "%s"\n'
% (abcisse, limite - 1,
minimum * 15 // 16, maximum * 17 // 16,
titre))
for ordonnee in data:
write('%d %d\n' % (abcisse, ordonnee))
abcisse += 1
write('e\n')
write('set multiplot\n'
'set size 1.0, 0.5\n'
'set origin 0.0, 0.5\n')
tracer("Mémoire CPU (K)", self.memoire_cpu,
self.memoire_cpu_min, self.memoire_cpu_max)
write('set size 1.0, 0.5\n'
'set origin 0.0, 0.0\n')
tracer("Nombre références", self.references,
self.references_min, self.references_max)
write('set nomultiplot\n')
def nouveau_gnuplot():
import os
if os.environ.get('DISPLAY'):
for repertoire in os.environ['PATH'].split(':'):
nom = repertoire + '/gnuplot'
if os.access(nom, os.X_OK):
gnuplot = os.popen(
# REVOIR: Comment faire ça tout en Python?
'sh -c \'trap "" 1; %s -persist\'' % nom, 'w')
gnuplot.write('set data style lines\n'
'set key right bottom\n')
return gnuplot
def main(*arguments):
def pause():
time.sleep(0.3)
import time
rapporter()
pause()
essai = Bulgroz(), Bulgroz(), Bulgroz()
rapporter('Rapport 1')
pause()
zorglub = [Zorglub() for compteur in range(50)]
rapporter('Rapport 2')
pause()
zorglub += list(essai)
rapporter('Rapport 3')
pause()
zorglub = zorglub[:10]
essai = None
rapporter('Rapport 4')
pause()
class Bulgroz: pass
class Zorglub(object): pass
if __name__ == '__main__':
import sys
main(*sys.argv[1:])
# Local Variables:
# compile-command: "python ressources.py"
# End:
--=-=-=
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 8bit
--
François Pinard http://www.iro.umontreal.ca/~pinard
--=-=-=--