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.