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.md dans 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 401 pour 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 dans build/api/) et l'ajouter en attendant comme dépendance compileOnly vers 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.gradle et le wrapper Gradle (./gradlew)
  • build.gradle avec 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.yml avec depend: [VoxelBench]
  • une classe principale qui enregistre un test d'exemple (spécifications typées de paramètre et de métrique, TestCompletion), un .gitignore et un README.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éthodeExé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 — un BenchmarkContext construit avec MockBenchmarkContext.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, et getInvocationCount() 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.dbLatencydbLatency
redisbench.getLatencylatency
metaplugin.tickProfiletps

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 publiques
  • docs/API_STABILITY.md — politique de versions et de stabilité
  • docs/examples/SampleExtensionPlugin.java — exemple complet en un seul fichier

Dépannage

SymptômeCause probable
VoxelBenchAPI not registered with Bukkit servicesdepend: [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 testL'enregistrement a échoué ; consultez les logs de démarrage
Un profil qui utilise votre test manque dans /bench custom list après un redémarrageLes profils sont lus avant que votre plugin enregistre ses tests ; lancez /bench custom reload
Le test tourne mais le champ provider manque dans le rapportVérifiez que vous enregistrez via api.getTestRegistry()
NoClassDefFoundError: BenchmarkTestVotre JAR embarque le package de l'API (utilisez compileOnly)
UnsupportedOperationException sous FoliaLe 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égionTravail sur le monde en dehors de ctx.runInZone / ctx.forEachZone