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:
- parse(fd)
- parse_file(filepath)
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).