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.
-
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.
-
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.
-
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.
<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
-
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 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 ?
-
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 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.