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.
-
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.
-
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.
-
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).
-
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.
-
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
}
}
-
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 ?
-
Ajouter une nouvelle implantation avec une méthode statique recordOf en utilisant
un record. Comparer les performances avec les implantations of et lambdaOf.
-
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().
-
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 ?
-
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.
-
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.