Zanbench : Release 0.2

Zanbench, framework actionscript 3 de test de performances est en version 0.2.

Vous pouvez lire l'introduction ou le démarrage rapide, télécharger un package comprenant les sources, le swc, la documentation et les exemples ou vous rendre sur la page d'accueil du projet.

Zanbench : démarrage rapide

Zanbench est un framework actionscript 3 de test de performances. Pour une introduction préliminaire, consultez cet article.

Dans cet article, nous allons voir comment installer et utiliser zanbench.

Installez zanbench

Téléchargez le swc depuis cette adresse et ajoutez le au classpath de votre projet.

Paramétrez le compilateur

Ajoutez la ligne ci-dessous aux options du compilateur. Attention, zanbench utilise la reflexion et ne peut fonctionner sans que le metatag [Benchmark] ne soit retenu après compilation.

-keep-as3-metadata+=Benchmark

Créez un BenchmarkCase

package gettingStarted
{
    import com.zanshine.benchmark.core.BenchmarkCase;

    public class SimpleBenchmarkCase extends BenchmarkCase
    {
    }
}

Créez un premier test

Créez une méthode publique, choisissez n'importe quel nom et taggez là avec [Benchmark]

package gettingStarted
{
    import com.zanshine.benchmark.core.BenchmarkCase;

    import flash.utils.getTimer;

    public class SimpleBenchmarkCase extends BenchmarkCase
    {
        [Benchmark]
        public function measureGetTimer():void
        {
            var whatTimeIsItPlease:Number = getTimer();
        }
    }
}

Créez une suite, ajoutez le benchmark à cette suite et lancez !

La méthode addBenchmark ci dessous prend comme argument :

  1. un objet implémentant Benchmarkable, ce qui est le cas de BenchmarkCase qui est la superclasse de SimpleBenchmarkCase
  2. un entier indiquant le nombre d'itérations de la boucle qui va appeller chaque méthode de test
  3. un entier indiquant le nombre de répétitions de la boucle
  4. un entier indiquant le délai en millisecondes entre chaque répétition
package
{
    import simple.SimpleBenchmarkCase;

    import com.zanshine.benchmark.core.BenchmarkSuite;
    import com.zanshine.benchmark.print.ResultPrinter;

    import flash.display.Sprite;

    public class GettingStartedRunner extends Sprite
    {
        public function GettingStartedRunner()
        {

            var suite:BenchmarkSuite = new BenchmarkSuite();
            suite.addBenchmark(new SimpleBenchmarkCase(), 1000, 500, 50);

            var printer:ResultPrinter = new ResultPrinter(suite);
            suite.run();
        }
    }
}

Les résultats sont visibles dans la console de sortie

benchmark case name => simple::SimpleBenchmarkCase
test method name    => measureGetTimer
message             =>
iterations per loop => 1000
loop run count      => 100
delay between loops => 50
raw duration        => 381

Ajoutez un second test au BenchmarkCase, décidez de l'ordre d'exécution et ajoutez un message par test

Zanbench offre la possibilité de définir l'orde dans lequel les méthodes de test doivent être appelées. Il est également possible de passer un message pour chaque méthode.

package gettingStarted
{
    import com.zanshine.benchmark.core.BenchmarkCase;

    import flash.utils.getTimer;

    public class SimpleBenchmarkCase extends BenchmarkCase
    {
        [Benchmark(order="2", message="This test will run in second")]
        public function measureGetTimer():void
        {
            var whatTimeIsItPlease:Number = getTimer();
        }

        [Benchmark(order=1, message="This test will run first")]
        public function measureDate():void
        {
            var whatTimeIsItPlease:Number = (new Date()).time;
        }
    }
}
benchmark case name => simple::SimpleBenchmarkCase
test method name    => measureDate
message             => This test will run first
iterations per loop => 1000
loop run count      => 100
delay between loops => 50
raw duration        => 713

benchmark case name => simple::SimpleBenchmarkCase
test method name    => measureGetTimer
message             => This test will run in second
iterations per loop => 1000
loop run count      => 100
delay between loops => 50
raw duration        => 388

Implémenter Benchmarkable si votre classe ne peut pas hériter de BenchmarkCase

Si pour une raison ou une autre, votre classe de benchmark ne peut pas hériter de BenchmarkCase, il vous suffit d'implémenter l'interface Benchmarkable, ce qui est très simple :

package gettingStarted
{
    import com.zanshine.benchmark.core.Benchmarkable;

    public class SharedObjectCase implements Benchmarkable
    {
        public function prepare():void
        {
        }

        public function setUp():void
        {
        }

        public function tearDown():void
        {
        }

        public function clean():void
        {
        }
    }
}

Il n'y a aucune implémentation obligatoire dans chacune de ces quatre méthodes. Vous pouvez les laisser vides et lancer le runner. Ces quatre méthodes sont des callbacks, des méthodes qui sont appelées automatiquement.

Définir des actions spécifiques à des moments clés de l'exécution du Benchmark

Zanbench prend en charge six callbacks différents :

  1. prepare():void : appelée à l'initialisation du Benchmarkable
  2. setUp():void : appelée avant chaque méthode de test
  3. beforeMethodName():void où le litéral "MethodName" correspond à une méthode de test methodName():void : appelée AVANT la méthode methodName():void
  4. afterMethodName():void où le litéral "MethodName" correspond à une méthode de test methodName():void : appelée APRES la méthode methodName():void
  5. tearDown():void appelée après chaque méthode de test
  6. clean():void appelée après toute les méthodes de test et les callbacks

Ces méthodes servent à manipuler, instancier ou détruire des fixtures. Pour ceux qui ne sont pas à l'aise avec les notions de base des framework xUnit, une fixture est un ensemble de préconditions et d'états nécessaires à l'exécution d'un test. Il s'agit du contexte d'exécution du test.

Voyons un exemple illustrant l'usage de ces callbacks (c'est bien sur un exemple didactique, sans prétentions architecturales :) ). Nous voulons mesurer la différence de temps d'exécution entre l'écriture d'une collection de Person sur un SharedObject local et un distant. D'abord, les deux classes du contexte :

package gettingStarted
{

    public class Person
    {
        public function Person()
        {
            People.registerPerson(this);
        }

        public var name:String;
        public var lastLogginDate:Date;
    }
}
package gettingStarted
{

    import flash.utils.Dictionary;
    public class People
    {
        private static var people:Dictionary = new Dictionary();
        public static function registerPerson(person:Person):void
        {
            people[person.name] = person;
        }

        public static function shotEveryOne():void
        {
            people = new Dictionary();
        }
    }
}

Voici la classe de test, les commentaires explicatifs sont ci-dessous.

package gettingStarted
{
    import flash.net.NetConnection;
    import flash.net.SharedObject;

    import com.zanshine.benchmark.core.Benchmarkable;

    public class SharedObjectCase implements Benchmarkable
    {
        private var so:SharedObject;
        private var connexion:NetConnection;
        private var contacts:Array;

        public function prepare():void
        {
            connexion = new NetConnection();
            connexion.connect("rtmp://somedomain.com/applicationName");
        }

        public function setUp():void
        {
            var paul:Person = new Person();
            paul.name = "Paul";
            paul.lastLogginDate = new Date();

            var luc:Person = new Person();
            luc.name = "Paul";
            luc.lastLogginDate = new Date();

            contacts = [];
            contacts.push(paul);
            contacts.push(luc);
        }

        public function tearDown():void
        {
            so.clear();
        }

        public function clean():void
        {
            People.shotEveryOne();
        }

        /**
         *  Measuring remote SharedObject writing time
         */
        public function beforeRemoteSharedObject():void
        {
            so = SharedObject.getRemote("sharedObjectCase");
            so.connect(connexion);
        }

        [Benchmark(message="remote shared object writing", order=2)]

        public function remoteSharedObject():void
        {
            writeSO();
        }

        public function afterRemoteSharedObject():void
        {
            so.close();
        }

        /**
         *  Measuring local SharedObject writing time
         */
        public function beforeLocalSharedObject():void
        {
            so = SharedObject.getLocal("sharedObjectCase");
        }

        [Benchmark(message="local shared object writing", order="1")]

        public function localSharedObject():void
        {
            writeSO();
        }

        /**
         * Generic write So helper
         */
        private function writeSO():void
        {
            for each(var p:Person in contacts)
            {
                so.data[p.name] = p;
            }
        }
    }
}

Ici, nous avons une classe Person, qui enregistre une référence d'elle même auprès de la classe People dans son constructeur. Instancier Person n'est donc pas sans conséquences, car cela laisse une trace au niveau de la classe People. Si nous avons d'autres tests qui dépendent d'une classe People dont le registre statique est vide, nos tests seront faussés. Nous devons donc veiller à détruire tous les objets Person créés avant de passer à un autre Benchmark, ce que nous faisons dans le callback clean(). Nous utilisons les callbacks "before" spécifiques à chaque méthode de test pour préparer correctement son contexte d'exécution. Après avoir terminé le test pour le shared object distant, nous fermons la connexion dans le callback "after". Puisque l'objet référencé par so change à chaque méthode de test, nous appelons so.clear() dans le callback tearDown() afin de ne laisser de traces du test ni en local, ni sur le serveur.

Zanbench : framework de test de performances

Zanbench est un framework opensource de test de performances ou "benchmark". Le design de zanbench s'inspire des framework xUnit. Le projet est hébergé sur google code et est actuellement en version 0.2. Dans cet article, je fais une brève introduction à ce qu'est un test de performance avant de vous orienter sur le démarrage rapide.

Pourquoi mesurer les performances ?

La mesure des performances, d'un système, d'un programme ou d'un algorithme est généralement liée à la volonté ou la nécessité d'optimiser le code. Je vous rapporte ici une citation que j'aime particulièrement concernant l'optimisation :

Règle 1: Ne pas optimiser.
Règle 2: Ne pas optimiser tant que nous n'avons pas de solutions parfaitement claire et non optimisée.
M. A. Jackson, Principles of Program Design

Toutefois, lorsque vient la phase d'optimisation d'un programme, nous sommes amenés à rechercher les noeuds de performances et à évaluer les différentes implémentations pouvant réduire les temps d'exécution. La mesure objective des performances d'un algorithme étant tributaire du système sur lequel il s'éxecute, on cherche généralement à mesurer sa vitesse d'exécution par rapport à un autre.

Lorsque l'on veut comparer la performance de tel ou tel autre algorithme, on est souvent contraint d'utiliser la méthode brute, à savoir mettre le code à tester dans une boucle for ou while et mesurer la différence de temps entre l'avant et l'après.

Un exemple de test de performance (benchmark) trivial, qui permet de comparer la performance de getTimer() par rapport à (new Date).time :

package simple
{
    import flash.display.Sprite;
    import flash.utils.getTimer;

    public class TimePerfTest extends Sprite
    {
        public function TimePerfTest()
        {
            measureGetTimer();
            measureDate();
        }

        public function measureGetTimer():void
        {
            var before:Number = getTimer();
            var whatTimeIsItPlease:Number;
            for (var i:int = 0;i < 1000000; i++)
            {
                whatTimeIsItPlease = getTimer();
            }
            var after:Number = getTimer();
            trace(after - before);//1469
        }

        public function measureDate():void
        {
            var before:Number = getTimer();
            var whatTimeIsItPlease:Number;
            for (var i:int = 0;i < 1000000; i++)
            {
                whatTimeIsItPlease = (new Date()).time;
            }
            var after:Number = getTimer();
            trace(after - before);//3728
        }
    }
}

Cette méthode, bien qu'efficace, présente plusieurs inconvénients. Tout d'abord, on peut vite se retrouver avec un block de code ayant un temps d'exécution supérieur à celui autorisé par le flashplayer, levant ainsi une erreur :

1502 Un script a été exécuté au-delà du délai d'expiration par défaut de 15 secondes

puis :

1503 Un script ne s'est pas arrêté après 30 secondes et a été arrêté.

Même si on évite ces erreurs en cassant la boucle entre plusieurs méthodes, il reste toujours le problème de l'usage des ressources systèmes. Une utilisation trop gourmande des ressources système, notamment processeur et mémoire vive, peut gravement nuire à la qualité des tests.

Si le système donne la priorité à un autre processus alors qu'il tourne à plein régime, les temps d'exécution de l'algorithme seront allongés de manière non représentative. De même, si le système se retrouve obligé de faire une lecture ou une écriture en mémoire virtuelle, du fait d'une utilisation trop importante de la mémoire vive par le flashplayer, les temps d'exécutions seront eux aussi affectés.

Effectuer des benchmarks crédibles demande donc de ménager le système, tout en répétant l'algorithme suffisamment de fois pour avoir un résultat représentatif. Finalement, cette dernière problématique peut aussi être contournée avec un peu de bon sens lors de l'écriture des tests, mais le code nécessaire devient vite redondant et difficile à maintenir.

Zanbench à pour objectif de permettre au développeur de s'abstraire du problème de l'écriture du test de performance, pour se focaliser sur le code à tester lui même. Quelques lignes de codes valant mieux qu'un long discours, rentrons dans le vif du sujet avec ce démarrage rapide.

Constructeur d'accès restreint en ActionScript 3

Alors que de nombreux design patterns reposent sur un constructeur d'accès restreint (protected, internal, private ou un namespace), le constructeur en actionscript 3 ne supporte à ce jour qu'une visibilité publique. Cette restriction rend difficile l'implémentation intègre de certains patterns pourtant très simple comme le singleton, la fabrique statique ou la classe abstraite (1). Je propose dans cet article un pattern qui permet de faire reposer la visibilité du constructeur sur un autre membre de classe, qui, lui, bénéficie de tous les modificateurs de visibilité existants en as3.

Parmi les constructeurs à accès restreint, le plus commun est le constructeur privé. Une implémentation communément rencontrée en as3 consiste à lever une exception dans le constructeur lui même, afin d'en verrouiller l'appel à l'exécution :

package com.zanshine.blog.restrictedConstructor
{

  public class NaivePrivateConstructor
  {
    public function NaivePrivateConstructor()
    {
      //Ici, le constructeur ne peut jamais être appelé.
      throw new Error("Private Constructor");
    }
  }
}

En dehors d'une classe statique ou abstraite, un tel constructeur n'est guère utilisable en l'état. Cette solution présente deux inconvénient majeurs : la restriction d'accès ne s'effectue qu'à l'exécution et l'héritage par appel explicite à super() n'est possible qu'au prix d'un appel à l'api de réflexion. Une implémentation de classe abstraite à l'aide de cette méthode :

package com.zanshine.blog.restrictedConstructor
{
  import flash.utils.getQualifiedClassName;

  public class NaiveAbstractClass
  {
    public function NaiveAbstractClass()
    {
      //on ne peut pas instancier directement cette classe
      if(getQualifiedClassName(this) ==
        "com.zanshine.blog.restrictedConstructor::NaiveAbstractClass")
        throw new Error("Abstract class instanciation");
    //reste de l'implémentation
    }
  }
}
package com.zanshine.blog.restrictedConstructor
{

  public class NaiveConcreteClass extends NaiveAbstractClass
  {
    public function NaiveConcreteClass()
    {
      //ici l'appel à super ne lève pas d'exception, puisque l'on sous-classe
      super();
      //reste de l'implémentation
    }
  }
}

Une alternative consiste à utiliser une clé, que l'on fournit au constructeur afin de valider la légitimité de l'appel. Si cette clé est nulle ou invalide, l'instanciation lève une exception. Deux solutions sont couramment utilisées, l'une étant une variante de l'autre : utiliser une fonction ou un objet comme clé. La validité de cette clé va être vérifiée dans le constructeur par test d'égalité de références et une exception sera levée le cas échéant.

La première variante, avec une fonction comme clé :

package com.zanshine.blog.restrictedConstructor
{

  public class RestrictedWithAFunction
  {
    public function RestrictedWithAFunction(key : Function)
    {
      if (key != constructorKey)
    throw new Error("Illegal instanciation");
    }

    private function constructorKey() : void;
  }
}

La seconde variante, avec un objet comme clé :

package com.zanshine.blog.restrictedConstructor
{

  public class RestrictedWithAnObject
  {
    public function RestrictedWithAnObject(key : *)//Il peut s'agir de n'importe quel type,
              //y compris une fonction, ce qui nous amènera d'ailleurs à la solution finale
    {
      if (key != constructorKey)
    throw new Error("Illegal instanciation");
    }

    private static var constructorKey : * = {};
    //on pourrait tout autant instancier [], function():void{},
    //new Function ou new MyCustomType, le mécanisme fonctionnerait.
  }
}

Voici un exemple d'implémentation de classes abstraite et concrète avec cette solution (je prends ici la seconde alternative, puisque la première, comme vous l'avez vu, n'est qu'une variante de la seconde, une fonction étant un objet) :

package com.zanshine.blog.restrictedConstructor
{

  public class WithAnObjectKeyAbstractClass
  {
    public function WithAnObjectKeyAbstractClass(key : *)
    {
      if (key != constructorKey)
    throw new Error("Illegal instanciation");
    }

    protected static var constructorKey : * = {};
  }
}
package com.zanshine.blog.restrictedConstructor
{

  public class WithAnObjectKeyConcreteClass extends WithAnObjectKeyAbstractClass
  {
    public function WithAnObjectKeyConcreteClass()
    {
      super(constructorKey);
    }
  }
}

Cette méthode pour restreindre l'accès au constructeur commence à gagner en élégance, mais ne permet toujours pas une vérification à la compilation. Il faut donc compter sur les tests unitaires pour soutenir la solution. Pour parfaire ce pattern de Constructeur d'Accès Restreint (Restricted Acces Constructor, RAC), il nous faut donc un typage fort et non générique de la clé. Ce typage permettra au compilateur de vérifier que la clé est bien du type attendu. Si ce type est connu de la seule classe à RAC, on garantit en grande partie la validité de la clé dès la compilation. L'actionscript 3 permettant la déclaration de classes interne, la solution se présente sous cette forme :

package com.zanshine.blog.restrictedConstructor
{

  public class RestrictedAccesConstructor
  {

    public function RestrictedAccesConstructor(key : Key)//(A)
    {
      if (!key)//(D)
    throw new Error("Illegal instanciation");
      //implémentation ...
    }

    private static var _constructorKey : Key = new Key; //(C)

    //(F)
    [public|protected|internal|private|namespace] static function get constructorKey() : Key {//(E)
      return _constructorKey;
    }
  }
}

internal final class Key {} //(B) Cette déclaration n'existe que pour RestrictedAccesConstructor

Le constructeur de RestrictedAccesConstructor prend en argument une référence à la classe interne Key (A) que seule RestrictedAccesConstructor est à même de fournir par export (B), puisque qu'aucune autre classe ne peut avoir une référence à Key par import. Une instance de cette classe interne sert donc de clé d'instanciation pour RestrictedAccesConstructor (C). A part si null est délibérément passé en argument au constructeur de RestrictedAccesConstructor, l'utilisation d'un type connu de cette seule classe pour la clé d'instanciation garantit la vérification dès la compilation. Il n'est donc plus nécessaire de tester l'égalité de références entre la clé connue et la clé donnée. Il ne reste qu'à vérifier la non nullité de la référence passée en argument afin d'assurer une vérification à l'exécution (D). Pour finir, il faut donner la possibilité à RestrictedAccesConstructor d'exporter sa clé vers les classes devant avoir le privilège de son instanciation (E). Ceci se fait via une méthode statique dont l'accès peut être maîtrisé par les modificateurs de visibilité natifs (public, protected, internal, private) ou les namespaces (F).

Une classe héritant de RestrictedAccessConstructor (qui illustre toujours l'exemple de classes abstraite/concrete) :

package com.zanshine.blog.restrictedConstructor
{
  import com.zanshine.blog.restrictedConstructor.RestrictedAccesConstructor;

  public class InheritFromRestrictedAccesConstructor extends RestrictedAccesConstructor
  {
    public function InheritFromRestrictedAccesConstructor()
    {
      super(constructorKey);
      //reste de l'implémentation
    }
  }
}

Voici un second exemple pour illustrer une autre visibilité de la clé (donc du constructeur) que protected. Ici, le constructeur est techniquement internal via le modificateur de visibilité de get constructorKey():Key :

package com.zanshine.blog.restrictedConstructor
{

  public class Car
  {
    public function Car(key : Key)
    {
      if(!key)
        throw new Error('The key reference passed to the
        constructor is null');
    }

    private static var _constructorKey : Key;

    //seules les classes du même package pourront accéder à la clé
    internal static function get constructorKey() : Key {
      return _constructorKey = _constructorKey ? _constructorKey : new Key;
    }
  }
}

internal final class Key {}
package com.zanshine.blog.restrictedConstructor
{

  public class CarFactory
  {
    public static function createCar() : Car
    {
      return new Car(Car.constructorKey);
    }
  }
}

Grâce à cette solution, de nombreuses possibilités s'ouvrent à nous pour implémenter les design patterns reposant sur un constructeur d'accès restreint. L'actionscript 3, au delà de ses lacunes, offre aussi des possibilités intéressantes. Dans le cas présent, je pense à l'usage des namespaces comme modificateurs de visibilité pour l'export de la clé d'instanciation. Les namespaces en as3 et leur usage débordent largement du cadre de cet article, mais je me ferais un plaisir d'en explorer les myriades de possibilités dans un autre post !

note :

1). Je range la classe abstraite dans la catégorie des des design patterns car, n'étant pas prise en charge nativement par l'actionscript 3, il nous faut passer par le design pour mettre le concept en pratique.