mercredi 22 février 2012

Bench de MoteurCsv

Suite à la publication du billet MoteurCsv le JPA du CSV, quelqu'un m'a demandé si j'avais fait un bench. N'en ayant pas fait, je me suis lancé.
Les sources du bench sont disponibles sur github

Génération du fichier de test

Le "cahier des charges" fournit par "bbo" était le suivant :
  • ~10 000 000 de lignes.
  • ~200 caractères par ligne.
  • ~20 champs (4/5 gros champs autour de 20 caractères.)
C'est avec ces contraintes que j'ai créé la classe GenerationFchierCsv.
Cette classe génère donc un fichier de 10 000 000 de lignes contenant des lignes avec 20 champs :
  • Quatre champs de type String et de 30 caractères
  • Quatre champs de type Boolean (1 ou 0)
  • Quatre champs de type Integer et de 5 chiffres
  • Quatre champs de type Double et de 5 chiffres avant la virgule et 5 chiffres après la virgule
  • Quatre champs de type String et de 5 caractères

Voici le résultat de la génération du fichier :
Statistiques sur la taille des lignes :
  • Minimum : 215 caractères
  • Maximum : 227 caractères
  • Moyenne : 225,666 caractères
Taille du fichier : 2 266 666 035 octets soit 2,11 Go
Génération en 70 576ms
Le résultat est donc à peu près conforme au cahier des charges.

Constitution du bench

Le bench est réalisé par la classe Bench (je sais, je suis trop bon pour trouver des noms).
Pour le bench, je ne pouvais pas utiliser la méthode permettant de parser tout le fichier et qui renvoie une liste d'objet. Vu la taille du fichier, il est facile de comprendre que le mettre entièrement en mémoire ne serait pas une bonne idée.
Heureusement le moteur contient une autre méthode permettant de réaliser un traitement pour chaque ligne : MoteurCsv.parseFileAndInsert
Dans le cadre du bench j'ai donc utilisé cette méthode en ne réalisant aucun traitement :
public static void bench1() throws FileNotFoundException {
    long startTime = System.currentTimeMillis();
    moteur.parseFileAndInsert(new FileReader(fichier), ObjetCsv.class,
        new InsertObject<objetcsv>() {
            @Override
            public void insertObject(ObjetCsv objet) {
                // On ne fait rien dans le cadre du bench.
            }
        });
    long elapsedTime = (System.currentTimeMillis() - startTime);
    System.out.println("Lecture du fichier : " + elapsedTime + "ms");
}

J'ai également créé une méthode permettant de voir l'utilisation mémoire au fur et à mesure du test.
Le but de cette méthode est de regarder la mémoire occupée avant et après un GC entre chaque itération du bench. Cela permettra de vérifier entre autre qu'il n'y ait pas fuite mémoire.
public static void gestionMemoire() {
    // Mémoire totale allouée
    long totalMemory = Runtime.getRuntime().totalMemory();
    // Mémoire utilisée
    long currentMemory = totalMemory - Runtime.getRuntime().freeMemory();
    System.out.println("Mémoire avant gc : " + (currentMemory / 1024) + "ko/" + (totalMemory / 1024) + "ko");
    System.gc();
    // Mémoire totale allouée
    totalMemory = Runtime.getRuntime().totalMemory();
    // Mémoire utilisée
    currentMemory = totalMemory - Runtime.getRuntime().freeMemory();
    System.out.println("Mémoire après gc : " + (currentMemory / 1024) + "ko/" + (totalMemory / 1024) + "ko");
}

Résultats

Les tests ont été menés sur un MacBookPro équipé d'un disque SSD et d'un Code i5.
Étape Temps Mémoire occupée avant GC Mémoire occupée après GC
Avant de commencer / 1 734ko 338ko
Instanciation du moteur 58 222µs 14 049ko 387ko
Lecture du fichier (itération 1) 65 902ms 14 049ko 387ko
Lecture du fichier (itération 2) 65 864ms 13 796ko 341ko
Lecture du fichier (itération 3) 64 638ms 14 059ko 341ko
Lecture du fichier (itération 4) 65 214ms 13 700ko 341ko
Lecture du fichier (itération 5) 66 560ms 14 027ko 341ko

Ces résultats permettent de montrer plusieurs choses :
  • Temps pour instancier le moteur : quasi-null
  • Mémoire persistante pour le moteur : quelques Ko
  • Performances plutôt satisfaisantes avec un peu plus d'une minute pour lire un fichier de plus de 2Go

J'ai également fait du profiling avec YourKit afin de vérifier le comportement interne du moteur, cela a montré que la majorité du temps est passé dans la librairie open-csv, l'overhead du moteur est donc plutôt faible.

Reproduire le bench

N'hésitez pas à reproduire le bench et à me dire les résultats que vous obtenez :
  • Cloner le projet depuis github : github.com/ybonnel/BenchMoteurCsv
  • Importer le projet en tant que projet maven dans Eclipse
  • Lancer d'abord le main de la classe GenerationFchierCsv afin de générer le fichier de test
  • Lancer ensuite le main de classe Bench afin de lancer le bench en lui-même
Si vous connaissez d'autres parseurs CSV n'hésitez pas à ajouter des benchs dans le projet pour comparer les performances avec d'autres parseurs.

lundi 20 février 2012

MoteurCsv le JPA du CSV

Voici un titre qui va faire gonfler mes chevilles :)
Je vais donc vous présenter une petite librairie Java sans prétention que j'ai réalisé afin de pourvoir lire et écrire des fichiers CSV facilement.

C'est quoi le CSV?

Bon pour la définition du format CSV, je vous invite à consulter l'article wikipedia.

Pourquoi cette librairie?

Cette librairie a été crée afin de pouvoir gérer le format GTFS. Ce format est utilisé afin de décrire un réseau de Transports en commun. Techniquement, c'est un zip contenant des fichiers CSV.
J'ai eu à utiliser ce format suite à l'ouverture des données de transports à Rennes où j'ai réalisé une application Android (Transports Rennes) permettant de consulter les horaires de bus.
A l'époque où j'ai créé cette librairie, je baignais dans du JPA la journée, et j'avoue que j'aime beaucoup l'idée de décrire du mapping directement dans la classe associé en utilisant des annotations.
C'est comme ça que MoteurCsv est né.

Alors comment ça s'utilise?

J'ai essayé de simplifier au maximun l'utilisation de la librairie.

Installation

Maven

Si vous êtes sous maven, l'intégration dans votre projet est très simple.
Il suffit d'ajouter dans votre pom.xml :
<dependencies>
    <dependency>
        <groupId>fr.ybo</groupId>
        <artifactId>moteurcsv</artifactId>
    </dependency>
</dependencies>

<repositories>
    <repository>
        <id>ybonnel-release</id>
        <url>https://repository-ybonnel.forge.cloudbees.com/release/</url>
    </repository>
</repositories>

Autre

Si vous n'êtes pas sous maven, il vous suffit d'intégrer les deux jars suivant :

Utilisation

Maintenant le vif du sujet, comment utiliser cette petite librairie.
Afin de pouvoir lire ou écrire un CSV il faut commencer par décrire la classe correspondante.
On va prendre pour l'exemple un CSV décrivant des personnes avec deux colonnes : "nom" et "prenom".
Voici un exemple correspondant :
nom,prenom
Bonnel,Yan
Vador,Dark
Noël,Père

Déclaration de la classe

Nous allons donc créer la classe Personne associée avec les annotations permettant de faire le mapping :

// Annotation permettant de dire au moteur que cette classe est associée à un fichier CSV.
@FichierCsv
public class Personne {

    // Annotation permettant de dire au moteur que cet attribut est mappé avec la colonne "nom" du CSV.
    @BaliseCsv("nom")
    private String nom;

    // Annotation permettant de dire au moteur que cet attribut est mappé avec la colonne "prenom" du CSV.
    @BaliseCsv("prenom")
    private String prenom;
}

Création du moteur

MoteurCsv moteur = new MoteurCsv(Personne.class);

Lecture d'un fichier CSV

Voici maintenant le code permettant de lire le fichier, et de le transformer en une liste d'objets :
InputStream stream = new FileInputStream(new File("personnes.csv"));
List<Personne> personnes = moteur.parseInputStream(stream, Personne.class);

Écriture d'un fichier CSV

Et le core permettant d'écrire un fichier :
Writer writer = new FileWriter(new File("personnes.csv"));
moteur.writeFile(writer, personnes, Personne.class);

Conclusion

J'espère que cette petite librairie sera utile à quelqu'un d'autre que moi :)
Elle est open-source (LGPL v3) : github.com/ybonnel/MoteurCsv
N'hésitez donc pas à forker et à faire des pull request!
Site généré contenant entre autre la javadoc : ybonnel.github.com/MoteurCsv

mercredi 8 février 2012

Transports Rennes - Résultat des votes

Après huit jours de votes (ici), voici les résultats pour le vote de la nouvelle icône de l'application Transports Rennes (Market).

J'ai tout d'abord été agréablement surpris par la participation : 96 votes!
En toute franchise, je m'attendais plutôt à une trentaine de votes max, donc merci à tous pour votre participation.

Voici les résultats :
Logo 01 - Cercle37 votes
Logo 07 - Etoiles17 votes
Logo 06 - Avec Ligne13 votes
Logo 00 - Actuel12 votes
Logo 02 - Cercle avec ombre7 votes
Logo 05 - Avec Panneau gris6 votes
Logo 03 - Station floutée3 votes
Logo 04 - Avec Panneau bleu1 votes

La prochaine icône de Transports Rennes sera donc :


Merci à l'auteur pour sa proposition (et ces 5 autres...) : Greg - @gregbzh35