From 59f33bd921fa24e5b85d6e34fa43ad711ba6dc88 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sat, 15 Aug 2026 12:04:28 -0400 Subject: [PATCH] probe- --- probe/build.gradle | 28 +- .../arcane/iris/probe/GenerationProbe.java | 513 +++++++++++++----- .../art/arcane/iris/probe/StubPlatform.java | 11 +- .../iris/probe/GenerationProbeTest.java | 81 +++ 4 files changed, 500 insertions(+), 133 deletions(-) create mode 100644 probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java diff --git a/probe/build.gradle b/probe/build.gradle index fff0b321d..896bcce0f 100644 --- a/probe/build.gradle +++ b/probe/build.gradle @@ -49,12 +49,12 @@ tasks.named('test').configure { dependsOn(':core:compileJava') } -String probePack = providers.gradleProperty('probePack') - .orElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/instances/purpur-26.2/plugins/Iris/packs/overworld') - .get() -String probeRadius = providers.gradleProperty('probeRadius').orElse('2').get() -String probeCenterChunkX = providers.gradleProperty('probeCenterChunkX').orElse('0').get() -String probeCenterChunkZ = providers.gradleProperty('probeCenterChunkZ').orElse('0').get() +Provider probePack = providers.gradleProperty('probePack') +Provider probeDimension = providers.gradleProperty('probeDimension') +Provider probeWarmupChunks = providers.gradleProperty('probeWarmupChunks').orElse('256') +Provider probeMeasuredChunks = providers.gradleProperty('probeMeasuredChunks').orElse('1024') +Provider probeCenterChunkX = providers.gradleProperty('probeCenterChunkX').orElse('2048') +Provider probeCenterChunkZ = providers.gradleProperty('probeCenterChunkZ').orElse('2048') tasks.register('genProbe', JavaExec) { group = 'verification' @@ -62,8 +62,22 @@ tasks.register('genProbe', JavaExec) { mainClass = 'art.arcane.iris.probe.GenerationProbe' classpath = sourceSets.main.runtimeClasspath jvmArgs('--add-modules', 'jdk.incubator.vector') - args(probePack, probeRadius, probeCenterChunkX, probeCenterChunkZ) dependsOn(':core:compileJava') + doFirst { + if (!probePack.isPresent() || probePack.get().isBlank()) { + throw new GradleException('genProbe requires -PprobePack=/absolute/path/to/pack') + } + if (!probeDimension.isPresent() || probeDimension.get().isBlank()) { + throw new GradleException('genProbe requires -PprobeDimension=') + } + args( + probePack.get(), + probeDimension.get(), + probeWarmupChunks.get(), + probeMeasuredChunks.get(), + probeCenterChunkX.get(), + probeCenterChunkZ.get()) + } } String deserializationPack = providers.gradleProperty('deserializationPack') diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java index 3cb5e4f76..292bca222 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -43,20 +43,23 @@ import art.arcane.iris.util.project.hunk.Hunk; import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HexFormat; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutorService; +import java.util.stream.Stream; public final class GenerationProbe { - private static final String DIMENSION_KEY = "overworld"; private static final long SEED = 1337L; - private static final int BIOME_STEP = 4; + private static final int SIGNATURE_SAMPLE_STEP = 8; private static final List REPORTED = Collections.synchronizedList(new ArrayList<>()); private static final class InertPreservation implements PreservationRegistry { @@ -123,57 +126,173 @@ public final class GenerationProbe { private static final class InertPlatformHooks implements EnginePlatformHooks { } - public static void main(String[] args) throws Exception { - StubPlatform.bindGenerationStateHandlers(); - StubPlatform.verbose(true); - StubPlatform.errorSink(REPORTED::add); - IrisServices.register(PreservationRegistry.class, new InertPreservation()); - IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager()); - IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new); - IrisServices.register(EnginePlatformHooks.class, new InertPlatformHooks()); - - File packSource = new File(args[0]); - int radius = args.length > 1 ? Integer.parseInt(args[1]) : 2; - int centerChunkX = args.length > 2 ? Integer.parseInt(args[2]) : 0; - int centerChunkZ = args.length > 3 ? Integer.parseInt(args[3]) : 0; - if (!packSource.isDirectory()) { - System.out.println("[genprobe] pack folder not found: " + packSource.getAbsolutePath()); - System.exit(1); - } - - File workRoot = Files.createTempDirectory("iris-genprobe").toFile(); - File pack = clonePack(packSource, workRoot); - System.out.println("[genprobe] pack: " + packSource.getAbsolutePath()); - System.out.println("[genprobe] work copy: " + pack.getAbsolutePath()); - System.out.println("[genprobe] center chunk: " + centerChunkX + "," + centerChunkZ); - System.out.println("[genprobe] radius: " + radius + " (" + ((2 * radius + 1) * (2 * radius + 1)) + " chunks)"); - - PackValidationResult validation = PackValidator.validate(pack); - for (String warning : validation.getWarnings()) { - System.out.println("[genprobe] pack warning: " + warning); - } - if (!validation.isLoadable()) { - for (String error : validation.getBlockingErrors()) { - System.out.println("[genprobe] pack error: " + error); + record ProbeConfiguration(File packSource, String dimensionKey, int warmupChunks, int measuredChunks, + int centerChunkX, int centerChunkZ) { + ProbeConfiguration { + if (packSource == null) { + throw new IllegalArgumentException("Pack folder is required."); } - System.out.println("[genprobe] FAIL: pack validation blocked generation"); - System.exit(1); + if (dimensionKey == null || dimensionKey.isBlank() || dimensionKey.chars().anyMatch(Character::isWhitespace)) { + throw new IllegalArgumentException("Dimension key must be non-blank and contain no whitespace."); + } + if (warmupChunks < 1) { + throw new IllegalArgumentException("Warmup chunk count must be at least 1."); + } + if (measuredChunks < 1) { + throw new IllegalArgumentException("Measured chunk count must be at least 1."); + } + } + + static ProbeConfiguration parse(String[] args) { + if (args.length != 6) { + throw new IllegalArgumentException("Expected: "); + } + return new ProbeConfiguration( + new File(args[0]), + args[1], + Integer.parseInt(args[2]), + Integer.parseInt(args[3]), + Integer.parseInt(args[4]), + Integer.parseInt(args[5])); + } + } + + record ChunkCoordinate(int x, int z) { + } + + record TimingSummary(long medianNanos, long p95Nanos, long maxNanos, long totalNanos) { + static TimingSummary from(List samples) { + if (samples.isEmpty()) { + throw new IllegalArgumentException("At least one timing sample is required."); + } + List sorted = new ArrayList<>(samples); + sorted.sort(Comparator.naturalOrder()); + int size = sorted.size(); + long median; + if ((size & 1) == 0) { + long lower = sorted.get((size / 2) - 1); + long upper = sorted.get(size / 2); + median = lower + ((upper - lower) / 2L); + } else { + median = sorted.get(size / 2); + } + int p95Index = Math.max(0, (int) Math.ceil(size * 0.95D) - 1); + long total = 0L; + for (long sample : samples) { + total += sample; + } + return new TimingSummary(median, sorted.get(p95Index), sorted.get(size - 1), total); + } + } + + record GenerationResult(int successfulChunks, int failedChunks, long firstChunkNanos, + TimingSummary measuredTimings, String signature) { + } + + record ProbeResult(String status, String dimensionKey, int warmupChunks, int measuredChunks, + int successfulChunks, int failedChunks, long engineReadyNanos, long firstChunkNanos, + TimingSummary measuredTimings, String signature) { + String machineLine() { + double measuredSeconds = measuredTimings.totalNanos() / 1_000_000_000D; + double chunksPerSecond = measuredSeconds == 0D ? 0D : measuredChunks / measuredSeconds; + return String.format(Locale.ROOT, + "IRIS_GENPROBE_RESULT version=1 status=%s dimension=%s warmup_chunks=%d measured_chunks=%d successful_chunks=%d failed_chunks=%d engine_ready_ms=%.3f first_chunk_ms=%.3f measured_median_ms=%.3f measured_p95_ms=%.3f measured_max_ms=%.3f measured_total_ms=%.3f measured_cps=%.3f signature=%s", + status, + dimensionKey, + warmupChunks, + measuredChunks, + successfulChunks, + failedChunks, + nanosToMillis(engineReadyNanos), + nanosToMillis(firstChunkNanos), + nanosToMillis(measuredTimings.medianNanos()), + nanosToMillis(measuredTimings.p95Nanos()), + nanosToMillis(measuredTimings.maxNanos()), + nanosToMillis(measuredTimings.totalNanos()), + chunksPerSecond, + signature); + } + } + + public static void main(String[] args) { + ProbeConfiguration configuration; + try { + configuration = ProbeConfiguration.parse(args); + } catch (Throwable e) { + System.out.println("[genprobe] FAIL: " + e.getMessage()); + e.printStackTrace(System.out); + System.exit(2); return; } - System.out.println("[genprobe] pack validation: PASS"); - IrisPlatforms.bind(new StubPlatform()); - - Engine engine; + int exitCode; try { - IrisData data = IrisData.get(pack); - IrisDimension dimension = data.getDimensionLoader().load(DIMENSION_KEY); - if (dimension == null) { - System.out.println("[genprobe] FAIL: dimension '" + DIMENSION_KEY + "' did not load from " + pack.getAbsolutePath()); - System.exit(1); - return; - } + ProbeResult result = run(configuration); + System.out.println(result.machineLine()); + exitCode = result.failedChunks() == 0 ? 0 : 1; + } catch (Throwable e) { + System.out.println("[genprobe] FAIL: probe execution failed"); + e.printStackTrace(System.out); + TimingSummary unavailable = new TimingSummary(0L, 0L, 0L, 0L); + ProbeResult result = new ProbeResult( + "FAIL", + configuration.dimensionKey(), + configuration.warmupChunks(), + configuration.measuredChunks(), + 0, + configuration.warmupChunks() + configuration.measuredChunks(), + 0L, + 0L, + unavailable, + "unavailable"); + System.out.println(result.machineLine()); + exitCode = 1; + } + System.exit(exitCode); + } + static List scheduleCoordinates(int count, int centerChunkX, int centerChunkZ) { + if (count < 1) { + throw new IllegalArgumentException("Chunk count must be at least 1."); + } + int width = (int) Math.ceil(Math.sqrt(count)); + int height = (count + width - 1) / width; + int startX = centerChunkX - (width / 2); + int startZ = centerChunkZ - (height / 2); + List coordinates = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + coordinates.add(new ChunkCoordinate(startX + (index % width), startZ + (index / width))); + } + return coordinates; + } + + private static ProbeResult run(ProbeConfiguration configuration) throws Throwable { + if (!configuration.packSource().isDirectory()) { + throw new IllegalArgumentException("Pack folder not found: " + configuration.packSource().getAbsolutePath()); + } + + File workRoot = Files.createTempDirectory("iris-genprobe-").toFile(); + IrisData data = null; + Engine engine = null; + Throwable executionFailure = null; + try { + File pack = clonePack(configuration.packSource(), workRoot); + configureProbeRuntime(workRoot); + validatePack(pack); + + System.out.println("[genprobe] pack: " + configuration.packSource().getAbsolutePath()); + System.out.println("[genprobe] dimension: " + configuration.dimensionKey()); + System.out.println("[genprobe] warmup chunks: " + configuration.warmupChunks()); + System.out.println("[genprobe] measured chunks: " + configuration.measuredChunks()); + System.out.println("[genprobe] center chunk: " + configuration.centerChunkX() + "," + configuration.centerChunkZ()); + + long engineStart = System.nanoTime(); + data = IrisData.get(pack); + IrisDimension dimension = data.getDimensionLoader().load(configuration.dimensionKey()); + if (dimension == null) { + throw new IllegalStateException("Dimension '" + configuration.dimensionKey() + + "' did not load from " + pack.getAbsolutePath()); + } IrisWorld world = IrisWorld.builder() .platformIdentity("iris:probe") .name("probe") @@ -184,70 +303,188 @@ public final class GenerationProbe { .build(); EngineTarget target = new EngineTarget(world, dimension, data); engine = new IrisEngine(target, IrisEngine.InitializationMode.RUNTIME); + long engineReadyNanos = System.nanoTime() - engineStart; + + List initNoise = settleAndDrain(); + printDistinctCauses("engine-init reported errors (non-fatal, async)", initNoise); + System.out.println("[genprobe] engine ready: dim=" + engine.getDimension().getLoadKey() + + " seed=" + engine.getSeedManager().getSeed() + + " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight() + + " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos))); + + GenerationResult generation = generate(engine, configuration); + String status = generation.failedChunks() == 0 ? "PASS" : "FAIL"; + printGenerationFailures(generation); + return new ProbeResult( + status, + configuration.dimensionKey(), + configuration.warmupChunks(), + configuration.measuredChunks(), + generation.successfulChunks(), + generation.failedChunks(), + engineReadyNanos, + generation.firstChunkNanos(), + generation.measuredTimings(), + generation.signature()); } catch (Throwable e) { - System.out.println("[genprobe] FAIL: engine construction threw before generation could start"); - e.printStackTrace(System.out); - printDistinctCauses("construction-time reported errors", drainReported()); - System.exit(1); - return; - } - - List initNoise = settleAndDrain(); - printDistinctCauses("engine-init reported errors (non-fatal, async)", initNoise); - - int height = engine.getTarget().getHeight(); - System.out.println("[genprobe] engine up: dim=" + engine.getDimension().getLoadKey() - + " seed=" + engine.getSeedManager().getSeed() - + " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight()); - - Map distinct = new LinkedHashMap<>(); - Map firstChunk = new LinkedHashMap<>(); - int ok = 0; - int failed = 0; - - for (int cz = centerChunkZ - radius; cz <= centerChunkZ + radius; cz++) { - for (int cx = centerChunkX - radius; cx <= centerChunkX + radius; cx++) { - String at = cx + "," + cz; - drainReported(); - List failures = new ArrayList<>(); - Hunk blocks = Hunk.newArrayHunk(16, height, 16); - Hunk biomes = Hunk.newArrayHunk(16, height, 16); - try { - engine.generate(cx << 4, cz << 4, blocks, biomes, false); - } catch (Throwable e) { - failures.add(e); - } - failures.addAll(drainReported()); - - if (failures.isEmpty()) { - ok++; - System.out.println("[genprobe] chunk " + at + " OK " + hashChunk(blocks, biomes, height)); + executionFailure = e; + throw e; + } finally { + Throwable cleanupFailure = closeProbe(engine, data, workRoot); + if (cleanupFailure != null) { + if (executionFailure != null) { + executionFailure.addSuppressed(cleanupFailure); } else { - failed++; - System.out.println("[genprobe] chunk " + at + " FAILED (" + failures.size() + " error(s))"); - for (Throwable failure : failures) { - failure.printStackTrace(System.out); - String key = causeKey(failure); - distinct.merge(key, 1, Integer::sum); - firstChunk.putIfAbsent(key, at); - } + throw cleanupFailure; } } } + } - System.out.println("[genprobe] generated OK: " + ok + ", failed: " + failed); - if (!distinct.isEmpty()) { - System.out.println("[genprobe] DISTINCT ROOT CAUSES (" + distinct.size() + "):"); - for (Map.Entry entry : distinct.entrySet()) { - System.out.println(" x" + entry.getValue() + " (first at chunk " + firstChunk.get(entry.getKey()) + ") " + entry.getKey()); + private static void configureProbeRuntime(File workRoot) { + REPORTED.clear(); + StubPlatform.bindGenerationStateHandlers(); + StubPlatform.verbose(false); + StubPlatform.errorSink(REPORTED::add); + IrisServices.register(PreservationRegistry.class, new InertPreservation()); + IrisServices.register(EngineWorldManagerProvider.class, + (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager()); + IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new); + IrisServices.register(EnginePlatformHooks.class, new InertPlatformHooks()); + IrisPlatforms.unbind(); + IrisPlatforms.bind(new StubPlatform(new File(workRoot, "platform-data"))); + } + + private static void validatePack(File pack) { + PackValidationResult validation = PackValidator.validateForDatapackBootstrap(pack); + for (String warning : validation.getWarnings()) { + System.out.println("[genprobe] pack warning: " + warning); + } + if (!validation.isLoadable()) { + for (String error : validation.getBlockingErrors()) { + System.out.println("[genprobe] pack error: " + error); + } + throw new IllegalStateException("Pack validation blocked generation."); + } + System.out.println("[genprobe] offline pack validation: PASS"); + } + + private static GenerationResult generate(Engine engine, ProbeConfiguration configuration) { + int totalChunks = configuration.warmupChunks() + configuration.measuredChunks(); + List coordinates = scheduleCoordinates( + totalChunks, configuration.centerChunkX(), configuration.centerChunkZ()); + List measuredTimings = new ArrayList<>(configuration.measuredChunks()); + MessageDigest signature = sha256(); + int successfulChunks = 0; + int failedChunks = 0; + long firstChunkNanos = 0L; + Map distinctFailures = new LinkedHashMap<>(); + Map firstFailureChunk = new LinkedHashMap<>(); + int height = engine.getTarget().getHeight(); + + for (int index = 0; index < totalChunks; index++) { + ChunkCoordinate coordinate = coordinates.get(index); + drainReported(); + List failures = new ArrayList<>(); + Hunk blocks = Hunk.newArrayHunk(16, height, 16); + Hunk biomes = Hunk.newArrayHunk(16, height, 16); + long started = System.nanoTime(); + try { + engine.generate(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, false); + } catch (Throwable e) { + failures.add(e); + } + long elapsed = System.nanoTime() - started; + failures.addAll(drainReported()); + + if (index == 0) { + firstChunkNanos = elapsed; + } + if (index >= configuration.warmupChunks()) { + measuredTimings.add(elapsed); + } + + if (failures.isEmpty()) { + successfulChunks++; + updateSignature(signature, coordinate, blocks, biomes, height); + } else { + failedChunks++; + recordFailures(coordinate, failures, distinctFailures, firstFailureChunk); + } + + int completed = index + 1; + if (completed == configuration.warmupChunks()) { + System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks"); + } else if (completed > configuration.warmupChunks() + && (completed == totalChunks || (completed - configuration.warmupChunks()) % 128 == 0)) { + System.out.println("[genprobe] measured progress: " + + (completed - configuration.warmupChunks()) + "/" + configuration.measuredChunks()); } } - System.out.println("[genprobe] RESULT: " + (failed == 0 ? "PASS" : "FAIL")); - System.exit(failed == 0 ? 0 : 1); + + if (!distinctFailures.isEmpty()) { + System.out.println("[genprobe] DISTINCT ROOT CAUSES (" + distinctFailures.size() + "):"); + for (Map.Entry entry : distinctFailures.entrySet()) { + System.out.println(" x" + entry.getValue() + " (first at chunk " + + firstFailureChunk.get(entry.getKey()) + ") " + entry.getKey()); + } + } + return new GenerationResult( + successfulChunks, + failedChunks, + firstChunkNanos, + TimingSummary.from(measuredTimings), + HexFormat.of().formatHex(signature.digest()).substring(0, 16)); + } + + private static void recordFailures(ChunkCoordinate coordinate, List failures, + Map distinctFailures, + Map firstFailureChunk) { + String coordinateLabel = coordinate.x() + "," + coordinate.z(); + System.out.println("[genprobe] chunk " + coordinateLabel + " FAILED (" + failures.size() + " error(s))"); + for (Throwable failure : failures) { + failure.printStackTrace(System.out); + String key = causeKey(failure); + distinctFailures.merge(key, 1, Integer::sum); + firstFailureChunk.putIfAbsent(key, coordinateLabel); + } + } + + private static void printGenerationFailures(GenerationResult generation) { + System.out.println("[genprobe] generated OK: " + generation.successfulChunks() + + ", failed: " + generation.failedChunks()); + System.out.println("[genprobe] first chunk ms: " + + String.format(Locale.ROOT, "%.3f", nanosToMillis(generation.firstChunkNanos()))); + System.out.println("[genprobe] measured median/p95/max ms: " + + String.format(Locale.ROOT, "%.3f/%.3f/%.3f", + nanosToMillis(generation.measuredTimings().medianNanos()), + nanosToMillis(generation.measuredTimings().p95Nanos()), + nanosToMillis(generation.measuredTimings().maxNanos()))); + } + + private static void updateSignature(MessageDigest digest, ChunkCoordinate coordinate, + Hunk blocks, Hunk biomes, int height) { + updateDigest(digest, coordinate.x() + "," + coordinate.z()); + int verticalStep = Math.max(1, height / 16); + for (int x = 0; x < 16; x += SIGNATURE_SAMPLE_STEP) { + for (int z = 0; z < 16; z += SIGNATURE_SAMPLE_STEP) { + for (int y = 0; y < height; y += verticalStep) { + PlatformBlockState state = blocks.get(x, y, z); + PlatformBiome biome = biomes.get(x, y, z); + updateDigest(digest, state == null ? "minecraft:air" : state.key()); + updateDigest(digest, biome == null ? "null" : biome.key()); + } + } + } + } + + private static void updateDigest(MessageDigest digest, String value) { + digest.update(value.getBytes(StandardCharsets.UTF_8)); + digest.update((byte) 0); } private static File clonePack(File source, File workRoot) throws Exception { - File destination = new File(workRoot, source.getName()); + File destination = new File(workRoot, "pack"); Process clone = new ProcessBuilder("cp", "-Rc", source.getAbsolutePath(), destination.getAbsolutePath()) .inheritIO() .start(); @@ -262,6 +499,53 @@ public final class GenerationProbe { return destination; } + private static Throwable closeProbe(Engine engine, IrisData data, File workRoot) { + Throwable failure = null; + if (engine != null) { + try { + engine.close(); + } catch (Throwable e) { + failure = e; + } + } + if (data != null) { + try { + data.close(); + } catch (Throwable e) { + failure = appendFailure(failure, e); + } + } + try { + deleteRecursively(workRoot.toPath()); + } catch (Throwable e) { + failure = appendFailure(failure, e); + } + return failure; + } + + private static Throwable appendFailure(Throwable failure, Throwable additional) { + if (failure == null) { + return additional; + } + if (failure != additional) { + failure.addSuppressed(additional); + } + return failure; + } + + private static void deleteRecursively(Path root) throws Exception { + if (!Files.exists(root)) { + return; + } + List paths; + try (Stream walk = Files.walk(root)) { + paths = walk.sorted(Comparator.reverseOrder()).toList(); + } + for (Path path : paths) { + Files.deleteIfExists(path); + } + } + private static List settleAndDrain() throws InterruptedException { List drained = new ArrayList<>(); long quietSince = System.currentTimeMillis(); @@ -330,31 +614,6 @@ public final class GenerationProbe { return trace.length > 0 ? trace[0].toString() : ""; } - private static String hashChunk(Hunk blocks, Hunk biomes, int height) { - MessageDigest blockDigest = sha256(); - MessageDigest biomeDigest = sha256(); - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - for (int y = 0; y < height; y++) { - PlatformBlockState state = blocks.get(x, y, z); - String key = state == null ? "minecraft:air" : state.key(); - blockDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8)); - } - } - } - for (int x = 0; x < 16; x += BIOME_STEP) { - for (int z = 0; z < 16; z += BIOME_STEP) { - for (int y = 0; y < height; y += BIOME_STEP) { - PlatformBiome biome = biomes.get(x, y, z); - String key = biome == null ? "null" : biome.key(); - biomeDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8)); - } - } - } - return HexFormat.of().formatHex(blockDigest.digest()).substring(0, 16) - + " " + HexFormat.of().formatHex(biomeDigest.digest()).substring(0, 16); - } - private static MessageDigest sha256() { try { return MessageDigest.getInstance("SHA-256"); @@ -362,4 +621,8 @@ public final class GenerationProbe { throw new IllegalStateException(e); } } + + private static double nanosToMillis(long nanos) { + return nanos / 1_000_000D; + } } diff --git a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java index c722f1703..d8aa68a6d 100644 --- a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java +++ b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java @@ -78,6 +78,15 @@ public final class StubPlatform implements IrisPlatform { private final StubScheduler scheduler = new StubScheduler(); private final StubStructureHooks structureHooks = new StubStructureHooks(); private final StubBiomeWriter biomeWriter = new StubBiomeWriter(); + private final File dataFolder; + + public StubPlatform() { + this(new File(System.getProperty("java.io.tmpdir"), "iris-probe")); + } + + public StubPlatform(File dataFolder) { + this.dataFolder = dataFolder; + } private static final class StubBlockState implements PlatformBlockState { private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); @@ -649,7 +658,7 @@ public final class StubPlatform implements IrisPlatform { @Override public File dataFolder() { - return new File(System.getProperty("java.io.tmpdir"), "iris-probe"); + return dataFolder; } @Override diff --git a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java new file mode 100644 index 000000000..96453f925 --- /dev/null +++ b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java @@ -0,0 +1,81 @@ +package art.arcane.iris.probe; + +import org.junit.Test; + +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public final class GenerationProbeTest { + @Test + public void parsesExplicitBenchmarkInputs() { + GenerationProbe.ProbeConfiguration configuration = GenerationProbe.ProbeConfiguration.parse(new String[]{ + "/tmp/pack", + "underworld", + "256", + "1024", + "2048", + "-2048" + }); + + assertEquals(new File("/tmp/pack"), configuration.packSource()); + assertEquals("underworld", configuration.dimensionKey()); + assertEquals(256, configuration.warmupChunks()); + assertEquals(1024, configuration.measuredChunks()); + assertEquals(2048, configuration.centerChunkX()); + assertEquals(-2048, configuration.centerChunkZ()); + } + + @Test + public void rejectsImplicitOrInvalidBenchmarkInputs() { + assertThrows(IllegalArgumentException.class, + () -> GenerationProbe.ProbeConfiguration.parse(new String[]{"/tmp/pack"})); + assertThrows(IllegalArgumentException.class, + () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), " ", 1, 1, 0, 0)); + assertThrows(IllegalArgumentException.class, + () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), "overworld", 0, 1, 0, 0)); + assertThrows(IllegalArgumentException.class, + () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), "overworld", 1, 0, 0, 0)); + } + + @Test + public void schedulesTheExactNumberOfUniqueChunksAroundTheCenter() { + List coordinates = GenerationProbe.scheduleCoordinates(1280, 2048, -2048); + Set distinct = new HashSet<>(coordinates); + + assertEquals(1280, coordinates.size()); + assertEquals(1280, distinct.size()); + assertTrue(coordinates.stream().allMatch(coordinate -> Math.abs(coordinate.x() - 2048) <= 18)); + assertTrue(coordinates.stream().allMatch(coordinate -> Math.abs(coordinate.z() + 2048) <= 18)); + } + + @Test + public void reportsMedianNearestRankP95MaximumAndTotal() { + GenerationProbe.TimingSummary odd = GenerationProbe.TimingSummary.from(List.of(50L, 10L, 30L, 20L, 40L)); + GenerationProbe.TimingSummary even = GenerationProbe.TimingSummary.from(List.of(40L, 10L, 30L, 20L)); + + assertEquals(30L, odd.medianNanos()); + assertEquals(50L, odd.p95Nanos()); + assertEquals(50L, odd.maxNanos()); + assertEquals(150L, odd.totalNanos()); + assertEquals(25L, even.medianNanos()); + } + + @Test + public void emitsAStableMachineReadableResultLine() { + GenerationProbe.TimingSummary timings = new GenerationProbe.TimingSummary( + 10_000_000L, 20_000_000L, 30_000_000L, 40_000_000L); + GenerationProbe.ProbeResult result = new GenerationProbe.ProbeResult( + "PASS", "underworld", 2, 4, 6, 0, + 5_000_000L, 6_000_000L, timings, "0123456789abcdef"); + + assertEquals( + "IRIS_GENPROBE_RESULT version=1 status=PASS dimension=underworld warmup_chunks=2 measured_chunks=4 successful_chunks=6 failed_chunks=0 engine_ready_ms=5.000 first_chunk_ms=6.000 measured_median_ms=10.000 measured_p95_ms=20.000 measured_max_ms=30.000 measured_total_ms=40.000 measured_cps=100.000 signature=0123456789abcdef", + result.machineLine()); + } +}