[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

--=-=-=--