Créer une extension
VoxelBench embarque 26 tests intégrés. Pour mesurer ce qui est propre à votre infrastructure — un pilote de base de données, un déploiement Redis, le chemin critique de votre plugin — vous pouvez publier un petit plugin Bukkit qui ajoute ses propres tests à VoxelBench grâce à son API d'extension publique.
L'API d'extension est en preview : elle est complète et versionnée, mais pas encore annoncée pour un usage tiers général. Sa politique de compatibilité est
docs/API_STABILITY.mddans le dépôt du plugin, privé pour l'instant (voir plus bas).
Vos tests apparaîtront dans :
/bench test <id>(et l'autocomplétion),- les profils YAML personnalisés (
/bench custom run mon-profil), - l'interface graphique, dans la catégorie Extensions du menu des tests,
- les rapports envoyés au backend, marqués du nom de votre plugin.
Prérequis
- Un JDK 17+ pour lancer Gradle (le plugin lui-même peut viser Java 16, comme VoxelBench)
- Un plugin Bukkit/Paper que vous maîtrisez
- VoxelBench installé sur le serveur cible
1. Ajouter la dépendance à l'API
L'API est publiée via JitPack, construite à partir
d'un tag de release de VoxelBench. La version est le tag git tel quel,
v initial compris (v1.9.0, et non 1.9.0). Épinglez la release qui
tourne sur votre serveur.
Pas encore disponible : le dépôt VoxelBench est privé pour l'instant, si bien que JitPack répond
401pour ces coordonnées tant qu'il n'est pas rendu public. Si vous avez accès au dépôt, vous pouvez construire vous-même le JAR de l'API avec./gradlew assembleApi(sortie dansbuild/api/) et l'ajouter en attendant comme dépendancecompileOnlyvers un fichier local.
Gradle
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
compileOnly 'com.github.Wasabules:voxelbench-plugin:v1.9.0'
}
La dépendance est en compileOnly : les classes de l'API existent déjà
dans le JAR de VoxelBench qui tourne sur le serveur ; les embarquer une
seconde fois provoquerait des conflits de class loader.
Maven
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.Wasabules</groupId>
<artifactId>voxelbench-plugin</artifactId>
<version>v1.9.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
Ou générer un projet prêt à compiler
Depuis une copie du dépôt VoxelBench :
./gradlew newExtension -PextName=MyBench -PtestId=mybench.cacheLatency
Cela crée extensions/MyBench/, un projet Gradle autonome qui se compile
seul :
settings.gradleet le wrapper Gradle (./gradlew)build.gradleavec l'API Spigot 1.17.1, l'API VoxelBench épinglée sur le dernier tag de release de votre copie, le plugin Shadow et les dépendances de test JUnit 6 + Mockito (plugin compilé pour Java 16, tests pour Java 17)plugin.ymlavecdepend: [VoxelBench]- une classe principale qui enregistre un test d'exemple (spécifications
typées de paramètre et de métrique,
TestCompletion), un.gitignoreet unREADME.md
Passez -PapiTag=v1.9.0 pour épingler une autre release (obligatoire si
aucun tag de release n'est trouvé, par exemple hors d'une copie git).
Sans -PtestId, l'identifiant vaut myext. suivi du nom de la classe (myext.myBench). Compilez le plugin avec
./gradlew shadowJar dans le nouveau dossier. La dépendance à l'API passe
par JitPack : la remarque ci-dessus s'applique.
2. Déclarer la dépendance dans plugin.yml
name: MyExtension
version: 1.0.0
main: com.example.MyExtension
api-version: '1.17'
# Dépendance forte : Bukkit garantit que VoxelBench se charge avant votre plugin.
# Utilisez plutôt `softdepend` si votre plugin doit fonctionner
# sans VoxelBench.
depend: [VoxelBench]
3. Implémenter un BenchmarkTest
import fr.wasabii.voxelBench.api.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public class MyDatabaseLatencyTest implements BenchmarkTest {
@Override public String getId() { return "myext.dbLatency"; }
@Override public String getDisplayName() { return "Database Latency"; }
@Override public TestCategory getCategory() { return TestCategory.EXTENSION; }
@Override public int getTimeoutSeconds() { return 60; }
@Override
public void run(BenchmarkContext ctx, Consumer<TestResult> callback) {
// Fin de test à appel unique, qui revient d'elle-même sur le thread serveur.
TestCompletion done = TestCompletion.of(ctx, callback);
int queries = ParamHelpers.paramInt(
ctx.getOwnerPlugin(), ctx.getParameters(),
"queries", 100, 1, 10_000, ctx.getSender());
// Les E/S bloquantes partent sur un thread Java ordinaire, jamais sur un
// thread serveur. N'utilisez PAS Bukkit.getScheduler() : indisponible sur Folia.
CompletableFuture.runAsync(() -> { // ou votre propre ExecutorService
double[] latencies = new double[queries];
long startNs = System.nanoTime();
for (int i = 0; i < queries; i++) {
if (ctx.isStopRequested()) {
done.failure("stopped");
return;
}
long t0 = System.nanoTime();
executeOneQuery(); // votre code
latencies[i] = (System.nanoTime() - t0) / 1_000_000.0;
}
double durationSec = (System.nanoTime() - startNs) / 1e9;
double mean = Arrays.stream(latencies).average().orElse(0);
Map<String, Object> metrics = new LinkedHashMap<>();
metrics.put("meanLatency",
RichMetric.of(mean)
.unit("ms").higherIsBetter(false)
.label("Mean query latency").precision(2).build());
metrics.put("perQuery",
RichMetricSeries.of(latencies)
.unit("ms").higherIsBetter(false)
.label("Per-query latency").precision(2).build());
done.success(durationSec, metrics);
});
}
}
Contrats essentiels :
run()est appelée sur un thread serveur : le thread principal sous Spigot/Paper, le thread de la région qui possède la zone de test sous Folia. Ne le bloquez jamais ; déplacez le travail bloquant sur un thread à vous.- Le résultat doit être livré une seule fois.
TestCompletion.of(ctx, callback)(marquée expérimentale) le garantit et ramène le résultat sur le thread serveur depuis n'importe quel thread. Si vous appelez vous-même le callback brut, faites-le une seule fois, depuis un thread serveur. - N'utilisez jamais
Bukkit.getScheduler()dans un test : Folia ne le prend pas en charge. Passez par les méthodes du contexte ci-dessous pour agir sur le monde. - Consultez régulièrement
ctx.isStopRequested()dans les boucles longues. - Redéfinissez
cleanup()si votre test crée des blocs, entités ou fichiers.
Agir sur le monde (compatible Folia)
Sous Folia, blocs et entités ne peuvent être manipulés que depuis le thread
de la région qui les possède. BenchmarkContext (API 1.2+) exécute votre
code au bon endroit sur chaque plateforme, si bien que le même test
fonctionne sous Spigot, Paper et Folia :
| Méthode | Exécute la tâche |
|---|---|
ctx.getZones() | Renvoie les zones de benchmark (une seule, ou plusieurs pour les runs à zones dispersées) |
ctx.runInZone(zone, task) | Sur la région qui possède zone (Folia), sur le thread principal ailleurs |
ctx.runInZoneLater(zone, task, delayTicks) | Idem, après un délai en ticks serveur |
ctx.forEachZone(perZone, onAllComplete) | Une fois par zone, en parallèle sous Folia, puis onAllComplete quand toutes les zones ont terminé |
ctx.isRegionized() | Indique si les zones tickent en parallèle (Folia) |
@Override
public void run(BenchmarkContext ctx, Consumer<TestResult> callback) {
TestCompletion done = TestCompletion.of(ctx, callback);
long startNs = System.nanoTime();
java.util.concurrent.atomic.AtomicInteger changed =
new java.util.concurrent.atomic.AtomicInteger();
ctx.forEachZone(zone -> {
// Ne toucher qu'aux blocs/entités autour de CETTE zone, de façon synchrone.
changed.addAndGet(placeBlocksAround(zone)); // votre code
}, () -> {
double sec = (System.nanoTime() - startNs) / 1e9;
done.success(sec, Map.of("blocksPerSecond", changed.get() / sec));
});
}
La tâche d'une zone est considérée comme terminée quand elle rend la main :
faites donc son travail de façon synchrone. Gardez la trace de ce que vous
créez et supprimez-le dans cleanup().
4. Enregistrer le test dans onEnable
public class MyExtension extends JavaPlugin {
@Override
public void onEnable() {
VoxelBenchAPI api = VoxelBenchAPI.getInstance();
api.getTestRegistry().register(
TestDescriptor.builder()
.id("myext.dbLatency")
.displayName("Database Latency")
.description("Round-trip latency to the primary database")
.category(TestCategory.EXTENSION)
.owner(getName()) // DOIT correspondre à plugin.yml
.builder(ctx -> new MyDatabaseLatencyTest())
.build(),
this);
}
}
Inutile de désenregistrer vos tests dans onDisable : VoxelBench écoute
PluginDisableEvent et retire automatiquement vos tests quand votre plugin
se désactive (arrêt du serveur, /reload, déchargement du plugin, ou même
un plantage dans votre propre onDisable).
5. Compiler, déployer, tester
Compilez votre plugin (./gradlew jar ou mvn package), déposez le JAR
dans plugins/ et redémarrez le serveur. Les logs de démarrage de
VoxelBench indiquent :
[VoxelBench] Loaded 1 extension test(s) from MyExtension: myext.dbLatency
Puis en jeu (les paramètres se passent en paires clé=valeur) :
/bench test myext.dbLatency
/bench test myext.dbLatency queries=500
Ou dans un profil YAML personnalisé (lancer un profil nécessite un serveur lié) :
# plugins/VoxelBench/custom_benchmarks/storage-suite.yml
name: "Storage Suite"
tests:
- id: disk
- id: myext.dbLatency
params:
queries: 500
/bench custom run storage-suite
VoxelBench lit les profils à son démarrage, avant que votre plugin (qui dépend de lui) ait enregistré ses tests. Un profil qui utilise un test d'extension est donc rejeté à chaque démarrage du serveur, avec references unknown test 'myext.dbLatency' dans la console : lancez /bench custom reload une fois le serveur démarré, puis lancez le profil.
Tester unitairement hors serveur
Le package fr.wasabii.voxelBench.api.testing (expérimental) permet
d'appeler run() depuis JUnit sans serveur Minecraft :
MockBenchmarkContext— unBenchmarkContextconstruit avecMockBenchmarkContext.builder(), où vous fixez les paramètres (.param("queries", 50)), le drapeau d'arrêt, le temps restant et, si besoin, les plugins, le monde, la zone ou l'émetteur.CapturingCompletion— un callback qui capture le résultat :getResult()pour les tests synchrones,awaitResult(Duration)pour les tests asynchrones, etgetInvocationCount()pour détecter une double livraison.
@Test
void dbLatency_reportsMeanLatency() throws Exception {
BenchmarkContext ctx = MockBenchmarkContext.builder()
.param("queries", 10)
.build();
CapturingCompletion done = new CapturingCompletion();
new MyDatabaseLatencyTest().run(ctx, done);
TestResult result = done.awaitResult(Duration.ofSeconds(10));
assertEquals(TestResult.Status.SUCCESS, result.getStatus());
assertTrue(result.getMetrics().containsKey("meanLatency"));
assertEquals(1, done.getInvocationCount());
}
Le mock ne simule ni mondes, ni chunks, ni entités : testez la logique de monde sur un vrai serveur.
Conventions d'identifiants
Choisissez un préfixe d'espace de noms pour éviter les collisions avec les tests intégrés ou d'autres extensions :
| ✅ Bon | ❌ Mauvais |
|---|---|
myext.dbLatency | dbLatency |
redisbench.getLatency | latency |
metaplugin.tickProfile | tps |
L'hôte normalise les identifiants en retirant tirets et underscores et en
passant en minuscules : myext.dbLatency, MyExt.DbLatency et
myext-db-latency désignent donc le même descripteur. Une collision lève
une exception explicite dès l'enregistrement.
Versions
La constante VoxelBenchAPI.API_VERSION est la version de l'API contre
laquelle votre plugin a été compilé ; l'API actuelle est en 1.2.0 (les
méthodes par région de BenchmarkContext sont apparues en 1.2).
api.getApiVersion() renvoie la version du VoxelBench installé sur le
serveur. Vérifiez-la à l'exécution si vous dépendez d'ajouts récents :
VoxelBenchAPI api = VoxelBenchAPI.getInstance();
if (!api.getApiVersionStructured().isAtLeast(1, 2)) {
getLogger().warning("VoxelBench " + api.getPluginVersion()
+ " is too old for this extension (need API 1.2+).");
getServer().getPluginManager().disablePlugin(this);
return;
}
isAtLeast(major, minor) renvoie aussi false quand la version majeure
diffère, puisqu'une nouvelle version majeure peut rompre la compatibilité.
Contrat de stabilité :
- Les interfaces STABLE (par défaut) ne cassent pas entre versions mineures ou correctives. Une version majeure peut casser, avec une entrée dans le changelog.
- Les types marqués EXPERIMENTAL peuvent changer à n'importe quelle version.
- Les types marqués INTERNAL ne font pas partie du contrat public.
Envoi au backend
Par défaut, les runs de profils personnalisés (tests d'extension compris)
restent locaux : rien n'est envoyé au backend. Si un profil déclare
submit: true, chaque entrée du tableau tests[] du rapport fournie par
votre extension porte un champ provider: "<nom de votre plugin>". Le
backend s'en sert pour tenir les résultats d'extension à l'écart du
classement officiel tout en les archivant pour le tableau de bord de
l'opérateur.
Le lancement unitaire (/bench test myext.dbLatency) est envoyé comme
rapport de test unitaire avec le même champ provider, lorsque le serveur
a activé la synchronisation des tests unitaires (reports.backend.unit-tests)
et qu'il est lié. Le backend peut aussi exclure ces rapports des classements
publics selon sa politique.
Voir aussi
Ces fichiers vivent dans le dépôt du plugin, privé pour l'instant ; ils seront liés ici quand il sera public.
docs/EXTENSION_API_REFERENCE.md— référence complète de l'API, toutes les classes publiquesdocs/API_STABILITY.md— politique de versions et de stabilitédocs/examples/SampleExtensionPlugin.java— exemple complet en un seul fichier
Dépannage
| Symptôme | Cause probable |
|---|---|
VoxelBenchAPI not registered with Bukkit services | depend: [VoxelBench] absent de plugin.yml |
TestDescriptor.owner '...' does not match the registering plugin | .owner(getName()) oublié ou valeur incorrecte |
Le test n'apparaît pas dans l'autocomplétion de /bench test | L'enregistrement a échoué ; consultez les logs de démarrage |
Un profil qui utilise votre test manque dans /bench custom list après un redémarrage | Les profils sont lus avant que votre plugin enregistre ses tests ; lancez /bench custom reload |
Le test tourne mais le champ provider manque dans le rapport | Vérifiez que vous enregistrez via api.getTestRegistry() |
NoClassDefFoundError: BenchmarkTest | Votre JAR embarque le package de l'API (utilisez compileOnly) |
UnsupportedOperationException sous Folia | Le test appelle Bukkit.getScheduler() ; utilisez les méthodes du contexte |
| Erreur Folia sur un accès au monde ou à une entité hors de sa région | Travail sur le monde en dehors de ctx.runInZone / ctx.forEachZone |