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.

3 commentaires:

Unknown a dit…

Le moteur n'est pas multi-threadé... et n'est pas multi-threadable nativement. Le seul moyen de faire du multi-thread serait de créer un pool de moteur, et d'envoyer les fichiers sur un moteur libre.
Si tu parlais de multi-thread pendant la lecture, là c'est plus compliqué et je vois pas bien ce que ça apporterai.
Pour les perfs, bien que j'en soit soucieux, ce n'était pas forcément l'objectif principal. Attention, je pense que le SSD aide beaucoup.

Unknown a dit…

Je suis d'accord avec le fait que ce ne soit pas le boulot du décodeur :)

Tu m'avais demandé si le moteur gérait de la validation de champs.
Je suis donc en train de l'ajouter :)
Par contre le package javax.validation n'est pas dans J2SE (donc encore moins dans Android, qui est l'une de mes plateformes d'utilisation), je vais donc pas l'utiliser. Mais je vais plutôt réinventer la roue.
Je vais ajouter la notion de obligatoire/facultatif directement sur l'annotation @BaliseCsv et ajouter une annotation @Validation permettant d'ajouter un Validateur sur tous les champs où on le souhaite. A partir de là on pourra ajouter les Validateur que l'on souhaite, qui devront juste implémenter un interface toute simple contenant une méthode "valid" qui renverra un exception si le champ n'est pas valide.
Qu'est ce que t'en pense?
(pour info, je commence le dev sur une branche spécifique : https://github.com/ybonnel/MoteurCsv/tree/validation)

Unknown a dit…

@bbo j'ai bientôt finit de coder la validation.
Si tu me donne un exemple de format CSV, je le code dans le bench avec la validation.

Enregistrer un commentaire