J'ai déjà parlé à plusieurs reprises sur ce blog de rss2email pour récupérer les flux rss. Avec plus d'une centaine de flux, il est un peu lent et consomme tout de même pas mal de CPU. Je me suis donc lancé dans un profilage du code pour comprendre ce qui consommait le plus.
Deux objectifs à ce billet :
- rapporter ma méthode
- demander à la communauté ce qu'on peut faire pour optimiser ce que j'ai identifié (cf la fin de ce billet)
Mise en place du profilage
On commence par cloner le dépôt de rss2email
git clone https://github.com/wking/rss2email
cd rss2email
ainsi que feedparser, la bibliothèque principale car je pressens qu'on va devoir y jeter un coup d'oeil.
git clone https://github.com/kurtmckee/feedparser.git
Je crée un virtualenv avec pew, comme suggéré par sametmax, c'est bien plus pratique.
pew new profiler
Construction et déploiement du code avec la méthode develop. Elle fait un lien symbolique, ce qui permet de ne pas avoir à faire une installation à chaque modification de code, ce qui est très intéressant lorsqu'on avance à petit pas dans le profilage.
cd feedparser
python setup.py develop
cd ..
# rss2email
python setup.py develop
On vérifie que ça tourne comme on l'attend. J'ai importé mes fichiers de conf et de base de données du serveur de prod pour être en condition de données réelles. L'option -n permet de faire un dry-run, c'est-à-dire de ne pas envoyer le courriel.
r2e -c rss2email.cfg run 30 -n
J'installe la bibliothèque qui permet de profiler le CPU. Voir cette référence que j'ai trouvé utile pour les différentes méthodes de profilage en python.
pip install line_profiler
Pour activer le profilage sur une fonction, il suffit d'utiliser un décorateur.
@profile
def func(var):
...
On lance ensuite kernprof
kernprof -l -v r2e -c rss2email.cfg run 30 -n
De manière récursive, je descend dans le code en répérant les fonctions qui prennent le plus de temps CPU.
Résultats
Le résultat est que le temps CPU est principalement utilisé par feedparser, que la moitié du temps sert à récupérer des données à travers le réseau (urllib) et l'autre moitié à faire du parsage de date. Ce parsage est traité en interne par feedparser.
Ces résultats sont rapportés dans ce ticket.
Regardons plus en détails. Il existe plusieurs formats à gérer. Pour mes flux, c'est principalement rfc822.
Le code commence par ceci
timezonenames = {
'ut': 0, 'gmt': 0, 'z': 0,
'adt': -3, 'ast': -4, 'at': -4,
'edt': -4, 'est': -5, 'et': -5,
'cdt': -5, 'cst': -6, 'ct': -6,
'mdt': -6, 'mst': -7, 'mt': -7,
'pdt': -7, 'pst': -8, 'pt': -8,
'a': -1, 'n': 1,
'm': -12, 'y': 12,
'met': 1, 'mest': 2,
}
def _parse_date_rfc822(date):
"""Parse RFC 822 dates and times
http://tools.ietf.org/html/rfc822#section-5
There are some formatting differences that are accounted for:
1. Years may be two or four digits.
2. The month and day can be swapped.
3. Additional timezone names are supported.
4. A default time and timezone are assumed if only a date is present.
"""
daynames = set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])
months = {
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12,
}
parts = date.lower().split()
Avec le profilage, j'ai vu que 10% du temps était utilisé à construire le set(). Les avantages de set sont de garantir l'unicité et d'avoir des opérations de type union ou intersection (comme le dit la doc). Ici, daynames n'est pas modifié, on n'a pas donc besoin de fabriquer l'unicité sur quelque chose qu'on définit. Seule une itération est faite sur cet élément. Le set() ne me semble donc pas justifié.
J'ai ouvert un pull request pour corriger le set en tuple, et passer la déclaration en variable globale.
Si vous avez des suggestions pour améliorer les performances, n'hésitez pas à les partager sur github ou par email. Merci !
Note : email.utils.parsedate_tz semble être plus lent que l'implémentation de feedparser, en terme CPU, c'est relativement comparable.