:: Enseignements :: ESIPE :: E4INFO :: 2020-2021 :: 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(RuntimeException | Error e) {
          throw e;
        } catch(Throwable 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, créer un sous-répertoire lab4, recopier dans celui-ci le fichier POM du lab3 et changer le contenu du POM de java-inside et du lab4 pour indiquer que le lab4 est un sous module.
  2. Copier/coller le code de l'interface Logger dans le package fr.umlv.javainside.
    Créer une classe de test JUnit 5 LoggerTests et écrire quelques tests vérifiant que le code de Logger fonctionne correctement.
    Ici, on ne vous demande pas d'écrire le code de la classe Logger donc si tous se passe bien, aucun test ne devrait passer.
    Note: 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.
    <dependencies>
      <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.25.2</version>
      </dependency>
      <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.25.2</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 reste 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.4</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
        

    Modifier le POM du lab4 et indiquer ce que veux dire "provided" pour un scope d'une dépendance.
  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 lambdaOf() 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. Ajouter une nouvelle implantation avec une méthode statique recordOf en utilisant un record. Comparer les performances avec les implantations of et lambdaOf.
  8. 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().
  9. 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 ?
  10. 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.
  11. Ecrire deux tests unitaires avec trois loggers, un qui émet un message, un qui n'émet pas de message et un qui est pas enable et faite en sorte que Travis exécute les tests.