Un parser pour le heap profiler de Valgrind

Irrité par les limitations du script ms_print.pl livré avec le heap profiler de Valgrind — Massif — j'ai développé une série de scripts Python remplaçant ce dernier. L'élément principal de cette suite d'outils est le module msparser.py qui permet d'extraire les données des fichiers générés par Massif. Son code est disponible sur github (MathieuTurcotte/msparser) et le paquet peut être téléchargé à partir de pypi (pypi/msparser).

msparser.py

L'interface ne comprend que deux fonctions:

La première prend en argument un descripteur de fichier alors que la seconde prend en argument un chemin vers un fichier massif.out.

>>> from pprint import pprint
>>> from msparser import parse_file
>>> data = parse_file('massif.out')
>>> pprint(data, depth=1)
{'cmd': './a.out',
 'desc': '--time-unit=ms',
 'detailed_snapshots_index': [...],
 'peak_snapshot_index': 16,
 'snapshots': [...],
 'time_unit': 'ms'}

Les champs 'detailed_snapshots_index' et 'peak_snapshot_index' permettent de localiser rapidement dans la liste 'snapshots' les snapshots détaillés ainsi que le snapshot enregistré alors de la consommation de mémoire du programme était maximale. Par exemple, pour localiser le premier snapshot détaillé, nous pourrions procéder de cette manière:

>>> detailed_snapshot_index = data['detailed_snapshots_index'][0]
>>> detailed_snapshot = data['snapshots'][detailed_snapshot_index]
>>> pprint(detailed_snapshot, depth=1)
{'heap_tree': {...},
 'id': 9,
 'mem_heap': 9000,
 'mem_heap_extra': 72,
 'mem_stack': 0,
 'time': 184}

Tel que l'exemple ci-dessus le laissait entendre, le champ 'snapshots' contient une liste de dictionnaires stockant chacun les données associées à un unique snapshot.

>>> first_snapshot = data['snapshots'][0]
>>> pprint(first_snapshot)
{'heap_tree': None,
 'id': 0,
 'mem_heap': 1000,
 'mem_heap_extra': 8,
 'mem_stack': 0,
 'time': 183}

Lorsqu'un snapshot est détaillé, cela signifie qu'une photographie complète du heap a été effectuée par Massif; le champ 'heap_tree' contient alors un arbre d'allocation détaillé du heap. Dans le cas contraire, comme le montre l'exemple ci-dessus, ce champ est nul.

>>> peak_index = data['peak_snapshot_index']
>>> peak_snapshot = data['snapshots'][peak_index]
>>> peak_heap_tree = peak_snapshot['heap_tree']
>>> pprint(peak_heap_tree, depth=3)
{'children': [{'children': [...], 'details': {...}, 'nbytes': 12000},
              {'children': [], 'details': {...}, 'nbytes': 10000},
              {'children': [...], 'details': {...}, 'nbytes': 8000},
              {'children': [...], 'details': {...}, 'nbytes': 2000}],
 'details': None,
 'nbytes': 32000}

Le champ 'details' de la racine de l'arbre d'allocation est toujours nul. Il en va de même pour les nœuds où la quantité de mémoire allouée est inférieure au threshold de Massif.

Gnuplot

Une fois les données extraites, générer un tableau de ces dernières est très simple.

print("# valgrind --tool=massif", data['desc'], data['cmd'])
print("# id", "time", "heap", "extra", "total", "stack", sep='\t')
for snapshot in data['snapshots']:
    id = snapshot['id']
    time = snapshot['time']
    heap = snapshot['mem_heap']
    extra = snapshot['mem_heap_extra']
    total = heap + extra
    stack = snapshot['mem_stack']
    print('  '+str(id), time, heap, extra, total, stack, sep='\t')
# valgrind --tool=massif --time-unit=ms ./a.out
# id    time    heap    extra   total   stack
  0     0       0       0       0       0
  1     183     1000    8       1008    0
  2     184     2000    16      2016    0
  3     184     3000    24      3024    0
  4     184     4000    32      4032    0
  5     184     5000    40      5040    0
  6     184     6000    48      6048    0
  7     184     7000    56      7056    0
  8     184     8000    64      8064    0
  9     184     9000    72      9072    0
...

Avec un peu d'aide de la part de Gnuplot, il est possible de produire des graphiques d'une très grande qualité à partir de ces données. Par exemple, les graphiques ci-dessous ont été générés à partir des profiles de consommation de mémoire de l'algorithme de Thompson et du résolveur de Sudoku basé sur la technique des Dancing Links.

Le code est disponible sur github (MathieuTurcotte/msparser) et le paquet sur pypi (pypi/msparser).