:: Enseignements :: ESIPE :: E4INFO :: 2019-2020 :: Java Inside ::
[LOGO]

Performance, CallSite et JMH


MethodHandle, MutableCallSite, etc

Exercice 1 - Logger

Le but de cet exercice est d'implanter une serie de Loggers qui ne ne consomment aucun temps de calcul s'ils ne sont pas activés.

Un logger implante l'interface Logger:
public interface Logger {
  public void log(String message);
  
  public static Logger of(Class<?> declaringClass, Consumer<? super String> consumer) {
    var mh = createLoggingMethodHandle(declaringClass, consumer);
    return new Logger() {
      @Override
      public void log(String message) {
        try {
          mh.invokeExact(message);
        } catch(Throwable t) {
          if (t instanceof RuntimeException) {
            throw (RuntimeException)t;
          }
          if (t instanceof Error) {
            throw (Error)t;
          }
          throw new UndeclaredThrowableException(t);
        }
      }
    };
  }
  
  private static MethodHandle createLoggingMethodHandle(Class<?> declaringClass, Consumer<? super String> consumer) {
    // TODO
    return null;
  } 
}
   

Le paramétre declaringClass de la méthode of correspond à la classe dans laquelle on déclare le Logger. Le paramètre consumer correspond à une fonction qui effectue l'affichage sur la console, dans un fichier, etc.
Par exemple, le code suivant créé un logger qui affiche les messages sur la sortie d'erreur standard.
class Foo {
  private static final Logger LOGGER = Logger.of(Foo.class, System.err::println);
  
  public void hello() {
    LOGGER.log("hello");  // print hello on System.err
  }
}
   

En terme d'implantation, chaque Logger utilise un method handle ce qui permet de specialiser son code à un usage spécifique.

  1. Dans le répertoire java-inside, utiliser Maven pour générer les fichiers pour le lab4
          mvn archetype:generate \
            -DarchetypeArtifactId=maven-archetype-quickstart \
            -DgroupId=fr.umlv.java.inside \
            -DartifactId=lab4  \
            -DinteractiveMode=false
        

    Recopier les properties, dependencies et les plugins de build du pom.xml du lab précédent.
  2. Copier/coller le code de l'interface Logger dans le package fr.umlv.javainside.lab4.
    Créer une classe de test JUnit 5 LoggerTests et écrire quelques tests vérifiant que le code de Logger fonctionne correctement.
    Note: vous n'avez pas besoin d'implanter la classe Logger pour cela, cela veut dire que pour l'instant les tests ne marchent pas et que le but est de les faire fonctionner.
    Note2: c'est ce que l'on appel faire du TDD.
  3. Ecrire la méthode createLoggingMethodHandle de la classe Logger et vérifier que les tests sont Ok.
    Pour l'instant, nous n'utiliserons pas le paramètre declaringClass.
    Note: pour créer un method handle sur le Consumer, vous pouvez utilisez findVirtual suivi d'un insertArguments (et un asType si les types ne correspondent pas).
  4. On souhaite maintenant faire un test de performance du code que vous venez d'écrire. Nous allons pour cela utiliser l'outil JMH.
    JMH a besoin de deux dépendances, jmh-core contient la librarie de test de performance et jmh-generator-annprocess qui est un processeur d'annotations qui va être exécuter par le compilateur.
    <properties>
      <jmh.version>1.21</jmh.version>
    </properties>
    <dependencies>
      <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>${jmh.version}</version>
      </dependency>
      <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>${jmh.version}</version>
        <scope>provided</scope>
      </dependency>
    </dependencies>    
        

    L'idée est que le processeur d'annotation va transformer le code pour isoler les tests de performance du reste du code pour éviter que le rete du code polue le résultat des tests de perf.
    Pour pouvoir executer les tests de perf, le plus simple est de demander à Maven de générer un jar avec tout dedans (JMH + votre code), pour cela on utilise le plugin shade
    <build>
      <plugins>
        <plugin> 
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-shade-plugin</artifactId>
          <version>3.2.0</version>
          <executions>
            <execution>
              <phase>package</phase>
              <goals>
                <goal>shade</goal>
              </goals>
              <configuration>
                <finalName>benchmarks</finalName>
                <transformers>
                  <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                    <mainClass>org.openjdk.jmh.Main</mainClass>
                  </transformer>
                </transformers>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
        

    Dans ce cas, on exécute les tests de la façon suivante
         java -jar target/benchmarks.jar
        
  5. Copier/coller le code du test de perf JMH ci-dessous (dans src/main/java/...) pour mesurer la performance de votre Logger dans le cas où le consommateur pris en paramètre est message -> { /*empty*/ }
    Attention à ce que votre Logger soit déclaré static final.
    import java.util.concurrent.TimeUnit;
    
    import org.openjdk.jmh.annotations.Benchmark;
    import org.openjdk.jmh.annotations.BenchmarkMode;
    import org.openjdk.jmh.annotations.Fork;
    import org.openjdk.jmh.annotations.Measurement;
    import org.openjdk.jmh.annotations.Mode;
    import org.openjdk.jmh.annotations.OutputTimeUnit;
    import org.openjdk.jmh.annotations.Scope;
    import org.openjdk.jmh.annotations.State;
    import org.openjdk.jmh.annotations.Warmup;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;    
        
    @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Fork(3)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class LoggerBenchMark {
      @Benchmark
      public void no_op() {
        // empty
      }
      
      @Benchmark
      public void simple_logger() {
        // TODO
      }
    }
        
  6. On peut remarquer que l'interface Logger est une interface fonctionnelle, car elle n'a qu'une méthode abstraite, on peut donc implanter un Logger avec une lambda au lieu d'une classe anonyme comme nous avons fait pour l'instant.
    Créer une méthode statique fastOf() qui fait exactement la même chose que of() mais utilise une lambda au lieu d'une classe anonyme.
    Modifier votre test JMH pour aussi tester cette nouvelle implantation.
    Y-a-t'il une différence de performance ? Pourquoi ?
  7. On souhaite pouvoir activer ou désactiver (avec un booléen) tous les loggers ayant la même declaringClass et ce même si les loggers ont déjà été créés.
    Pour cela on se propose d'ajouter le code suivant
    private static final ClassValue<MutableCallSite> ENABLE_CALLSITES = new ClassValue>() {
      protected MutableCallSite computeValue(Class<?> type) {
        return new MutableCallSite(MethodHandles.constant(boolean.class, true));
      }
    };
    
    public static void enable(Class<?> declaringClass, boolean enable) {
      ENABLE_CALLSITES.get(declaringClass).setTarget(MethodHandles.constant(boolean.class, enable)));
    }
        

    Sachant qu'il est possible de créer un method handle à partir d'un MutableCallSite en appelant la méthode dynamicInvoker, l'idée est de modifier le code de createLoggingMethodHandle pour utiliser le method handle renvoyé par dynamicInvoker() comme test d'un guardWithTest.
    Si la valeur est vrai, le guardWithTest exécute le méthod handle qui log le message et si la valeur est fausse le guardWithTest exécute un méthode handle vide (créer en utlisant MethodHandles.empty().
  8. Ajouter un test JMH qui crée un Logger que vous désactiverez dans le bloc statique et tester sa vitesse, que pouvez vous en conclure ?
  9. Que se passe t'il, si il y a plusieurs threads ? si une thread appel enable() et un autre thread appel log() ?
    Pour cela aller lire la doc de MutableCallSite et faite les changements qui s'impose pour que le code marche avec plusieurs threads.
  10. Ecrire deux tests unitaires avec trois loggers, un qui emet un message, un qui n'emet pas de message et un qui est pas enable et faite en sorte que Travis exécute les tests.