From 217ed04f457849d160f591fd6a2931b3058bb513 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 19:45:12 -0400 Subject: [PATCH] Split probe helpers - Move chunk generation and buffer selection into ProbeChunkGenerator - Move measured-window stage and counter lines into ProbeWindowMetrics - Move hydrology pre-planning into ProbeHydrologyPreplan --- .../arcane/iris/probe/GenerationProbe.java | 216 ++---------------- .../iris/probe/ProbeChunkGenerator.java | 98 ++++++++ .../iris/probe/ProbeHydrologyPreplan.java | 109 +++++++++ .../arcane/iris/probe/ProbeWindowMetrics.java | 67 ++++++ .../iris/probe/GenerationProbeTest.java | 10 +- 5 files changed, 298 insertions(+), 202 deletions(-) create mode 100644 probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java create mode 100644 probe/src/main/java/art/arcane/iris/probe/ProbeHydrologyPreplan.java create mode 100644 probe/src/main/java/art/arcane/iris/probe/ProbeWindowMetrics.java 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 547d667c4..2e52255d1 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -22,26 +22,20 @@ import art.arcane.iris.pack.datapack.DataVersion; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.pack.PackValidator; -import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineEffects; import art.arcane.iris.generation.runtime.EngineEffectsProvider; -import art.arcane.iris.generation.runtime.EngineMetrics; import art.arcane.iris.generation.runtime.EnginePlatformHooks; import art.arcane.iris.generation.runtime.EngineTarget; import art.arcane.iris.generation.runtime.EngineWorldManager; import art.arcane.iris.generation.runtime.EngineWorldManagerProvider; -import art.arcane.iris.generation.runtime.GenerationCounter; -import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.runtime.MeteredCache; import art.arcane.iris.generation.runtime.PreservationRegistry; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.generation.concurrent.MultiBurst; -import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings; import art.arcane.iris.generation.hydrology.HydrologyTileCache; -import art.arcane.iris.generation.hydrology.HydrologyTileKey; import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.world.IrisWorld; @@ -53,7 +47,6 @@ import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisServices; import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; -import art.arcane.volmlib.util.cache.CacheContention; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.noise.FractalBillowSimplexNoise; @@ -72,12 +65,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; public final class GenerationProbe { private static final List REPORTED = Collections.synchronizedList(new ArrayList<>()); @@ -278,29 +271,6 @@ public final class GenerationProbe { TimingSummary measuredTimings, String signature) { } - record WindowMetrics(Map stages, Map counters) { - static WindowMetrics capture(Engine engine, boolean counters) { - return new WindowMetrics(engine.getMetrics().stageTotals(), counters ? counterValues() : Map.of()); - } - - private static Map counterValues() { - Map values = new LinkedHashMap<>(); - for (GenerationCounter counter : GenerationCounter.values()) { - values.put(counter.key(), counter.sum()); - } - values.put("cache_duplicate_computations", CacheContention.duplicateComputations()); - return values; - } - } - - record HydrologyPreplan(int tiles, int tileSize, int publicationRadius, int reachChunks, long elapsedNanos) { - String machineLine() { - return String.format(Locale.ROOT, - "IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=%d tile_size=%d publication_radius=%d reach_chunks=%d planned_ms=%.3f", - tiles, tileSize, publicationRadius, reachChunks, nanosToMillis(elapsedNanos)); - } - } - record ProbeResult(String status, String dimensionKey, int warmupChunks, int measuredChunks, boolean multicore, boolean studio, int successfulChunks, int failedChunks, long engineReadyNanos, long firstChunkNanos, @@ -329,51 +299,6 @@ public final class GenerationProbe { } } - static String stagesLine(WindowMetrics start, WindowMetrics end, int chunks, int threads) { - StringBuilder line = new StringBuilder(512); - line.append("IRIS_GENPROBE_STAGES version=1 chunks=").append(chunks).append(" threads=").append(threads); - for (Map.Entry entry : end.stages().entrySet()) { - EngineMetrics.StageTotal earlier = start.stages().get(entry.getKey()); - EngineMetrics.StageTotal window = earlier == null ? entry.getValue() : entry.getValue().since(earlier); - line.append(' ').append(entry.getKey()).append('=') - .append(String.format(Locale.ROOT, "%.3f", window.millis())); - } - return line.toString(); - } - - static String countersLine(WindowMetrics start, WindowMetrics end, int chunks) { - StringBuilder line = new StringBuilder(256); - line.append("IRIS_GENPROBE_COUNTERS version=1 chunks=").append(chunks); - for (Map.Entry entry : end.counters().entrySet()) { - long window = entry.getValue() - start.counters().getOrDefault(entry.getKey(), 0L); - line.append(' ').append(entry.getKey()).append('=').append(window) - .append(' ').append(entry.getKey()).append("_per_chunk=") - .append(String.format(Locale.ROOT, "%.1f", chunks == 0 ? 0D : window / (double) chunks)); - } - return line.toString(); - } - - static List preplanTiles(ProbeConfiguration configuration, int tileSize, int publicationRadius, - int reachChunks) { - HydrologyTileCache.PregenerationArea area = pregenerationArea(configuration); - long reach = (long) reachChunks * 16L + publicationRadius; - int minimumX = Math.toIntExact(Math.floorDiv(area.minimumBlockX() - reach, tileSize)) - 1; - int minimumZ = Math.toIntExact(Math.floorDiv(area.minimumBlockZ() - reach, tileSize)) - 1; - int maximumX = Math.toIntExact(Math.floorDiv(area.maximumBlockX() + reach, tileSize)) + 1; - int maximumZ = Math.toIntExact(Math.floorDiv(area.maximumBlockZ() + reach, tileSize)) + 1; - int centerX = Math.floorDiv(area.centerBlockX(), tileSize); - int centerZ = Math.floorDiv(area.centerBlockZ(), tileSize); - List keys = new ArrayList<>((maximumX - minimumX + 1) * (maximumZ - minimumZ + 1)); - for (int tileZ = minimumZ; tileZ <= maximumZ; tileZ++) { - for (int tileX = minimumX; tileX <= maximumX; tileX++) { - keys.add(new HydrologyTileKey(tileX, tileZ)); - } - } - keys.sort(Comparator.comparingInt((HydrologyTileKey key) -> - Math.max(Math.abs(key.tileX() - centerX), Math.abs(key.tileZ() - centerZ)))); - return keys; - } - public static void main(String[] args) { ProbeConfiguration configuration; ProbeOptions options; @@ -508,7 +433,7 @@ public final class GenerationProbe { + " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos))); System.out.println("IRIS_GENPROBE_MANTLE version=1 radius=" + engine.getMantle().getRadius() + " real_radius=" + engine.getMantle().getRealRadius()); - ProbeGenerator generator = new ProbeGenerator(engine, router, options.productionBuffers()); + ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers()); long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount(); GenerationResult generation = generateWithProfile(generator, configuration, options); verifyBackend(nativeSamplesBefore); @@ -598,7 +523,7 @@ public final class GenerationProbe { System.out.println("[genprobe] offline pack validation: PASS"); } - private static GenerationResult generateWithProfile(ProbeGenerator generator, ProbeConfiguration configuration, + private static GenerationResult generateWithProfile(ProbeChunkGenerator generator, ProbeConfiguration configuration, ProbeOptions options) throws Exception { Engine engine = generator.engine(); if (!options.pregenProfile()) { @@ -621,48 +546,10 @@ public final class GenerationProbe { if (!options.hydrologyPreplan()) { return; } - IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime(); - if (hydrology == null) { - System.out.println("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=0 skipped=no-hydrology"); - return; - } - HydrologyPlannerSettings settings = hydrology.settings(); - int tileSize = settings.routing().tileSize(); - int publicationRadius = settings.publicationRadius(); - int reachChunks = Math.max(engine.getMantle().getRadius(), engine.getMantle().getRealRadius()); - List keys = preplanTiles(configuration, tileSize, publicationRadius, reachChunks); - long started = System.nanoTime(); - ExecutorService executor = Executors.newFixedThreadPool(Math.min(keys.size(), 4)); - try { - List> plans = new ArrayList<>(keys.size()); - for (HydrologyTileKey key : keys) { - plans.add(executor.submit(() -> hydrology.tile(key))); - } - for (Future plan : plans) { - awaitPlan(plan); - } - } finally { - executor.shutdown(); - } - HydrologyPreplan preplan = new HydrologyPreplan(keys.size(), tileSize, publicationRadius, reachChunks, - System.nanoTime() - started); + Optional preplan = ProbeHydrologyPreplan.run(engine, configuration); printDistinctCauses("hydrology preplan reported errors", drainReported()); - System.out.println(preplan.machineLine()); - } - - private static void awaitPlan(Future plan) throws Exception { - try { - plan.get(); - } catch (ExecutionException failure) { - Throwable cause = failure.getCause(); - if (cause instanceof Error error) { - throw error; - } - if (cause instanceof Exception exception) { - throw exception; - } - throw new IllegalStateException(cause); - } + System.out.println(preplan.map(ProbeHydrologyPreplan::machineLine) + .orElse("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=0 skipped=no-hydrology")); } static HydrologyTileCache.PregenerationArea pregenerationArea(ProbeConfiguration configuration) { @@ -708,7 +595,7 @@ public final class GenerationProbe { return Math.atan2(coordinate.z() - centerChunkZ, coordinate.x() - centerChunkX); } - private static GenerationResult generate(ProbeGenerator generator, ProbeConfiguration configuration, + private static GenerationResult generate(ProbeChunkGenerator generator, ProbeConfiguration configuration, ProbeOptions options) throws Exception { Engine engine = generator.engine(); int totalChunks = configuration.warmupChunks() + configuration.measuredChunks(); @@ -727,16 +614,16 @@ public final class GenerationProbe { } } - private static void generateSequential(ProbeGenerator generator, ProbeConfiguration configuration, + private static void generateSequential(ProbeChunkGenerator generator, ProbeConfiguration configuration, ProbeOptions options, List coordinates, OutcomeSink sink) throws IOException { int totalChunks = coordinates.size(); long measuredStartEpochMillis = 0L; - WindowMetrics measuredStart = null; + ProbeWindowMetrics measuredStart = null; for (int index = 0; index < totalChunks; index++) { if (index == configuration.warmupChunks()) { measuredStartEpochMillis = System.currentTimeMillis(); - measuredStart = WindowMetrics.capture(generator.engine(), options.counters()); + measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters()); } drainReported(); ChunkOutcome outcome = generator.generate(index, coordinates.get(index), configuration.multicore()); @@ -745,14 +632,15 @@ public final class GenerationProbe { printProgress(configuration, index + 1, totalChunks); } printWindow(measuredStartEpochMillis, System.currentTimeMillis()); - printWindowMetrics(measuredStart, WindowMetrics.capture(generator.engine(), options.counters()), + printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()), configuration.measuredChunks(), options); } - private static void printWindowMetrics(WindowMetrics start, WindowMetrics end, int chunks, ProbeOptions options) { - System.out.println(stagesLine(start, end, chunks, options.threads())); + private static void printWindowMetrics(ProbeWindowMetrics start, ProbeWindowMetrics end, int chunks, + ProbeOptions options) { + System.out.println(ProbeWindowMetrics.stagesLine(start, end, chunks, options.threads())); if (options.counters()) { - System.out.println(countersLine(start, end, chunks)); + System.out.println(ProbeWindowMetrics.countersLine(start, end, chunks)); } } @@ -761,7 +649,7 @@ public final class GenerationProbe { + " end_epoch_ms=" + endEpochMillis); } - private static void generateParallel(ProbeGenerator generator, ProbeConfiguration configuration, + private static void generateParallel(ProbeChunkGenerator generator, ProbeConfiguration configuration, ProbeOptions options, List coordinates, OutcomeSink sink) throws Exception { ChunkOutcome[] outcomes = new ChunkOutcome[coordinates.size()]; @@ -784,13 +672,13 @@ public final class GenerationProbe { List measured = spiralOrder(coordinates, configuration.warmupChunks(), configuration.centerChunkX(), configuration.centerChunkZ()); - WindowMetrics measuredStart = WindowMetrics.capture(generator.engine(), options.counters()); + ProbeWindowMetrics measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters()); long measuredStartEpochMillis = System.currentTimeMillis(); long measuredStarted = System.nanoTime(); runPhase(executor, generator, configuration, coordinates, measured, outcomes); measuredWallNanos = System.nanoTime() - measuredStarted; printWindow(measuredStartEpochMillis, System.currentTimeMillis()); - printWindowMetrics(measuredStart, WindowMetrics.capture(generator.engine(), options.counters()), + printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()), configuration.measuredChunks(), options); unattributed.addAll(drainReported()); System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/" @@ -825,7 +713,7 @@ public final class GenerationProbe { System.out.println(throughput.machineLine()); } - private static void runPhase(ExecutorService executor, ProbeGenerator generator, + private static void runPhase(ExecutorService executor, ProbeChunkGenerator generator, ProbeConfiguration configuration, List coordinates, List order, ChunkOutcome[] outcomes) throws Exception { List> futures = new ArrayList<>(order.size()); @@ -1057,72 +945,6 @@ public final class GenerationProbe { return nanos / 1_000_000D; } - static final class ProbeGenerator { - private final Engine engine; - private final GenerationHistoryRuntimeRouter router; - private final boolean productionBuffers; - private final AtomicInteger expandedBiomeHunks = new AtomicInteger(); - - ProbeGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) { - this.engine = engine; - this.router = router; - this.productionBuffers = productionBuffers; - } - - Engine engine() { - return engine; - } - - int expandedBiomeHunks() { - return expandedBiomeHunks.get(); - } - - ChunkOutcome generate(int index, ChunkCoordinate coordinate, boolean multicore) { - int height = engine.getTarget().getHeight(); - List failures = new ArrayList<>(); - Hunk blocks = newBlocks(height); - Hunk biomes = newBiomes(height); - long started = System.nanoTime(); - try { - generateInto(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, multicore); - } catch (Throwable e) { - failures.add(e); - } - long elapsed = System.nanoTime() - started; - if (biomes instanceof ProductionBiomeHunk production && production.expanded()) { - expandedBiomeHunks.incrementAndGet(); - } - return new ChunkOutcome(index, coordinate, elapsed, blocks, biomes, failures); - } - - Hunk newBlocks(int height) { - return productionBuffers ? new ProductionBlockHunk(height) : Hunk.newArrayHunk(16, height, 16); - } - - Hunk newBiomes(int height) { - return productionBuffers ? new ProductionBiomeHunk(height, ProbeGenerator::plainsBiome) - : Hunk.newArrayHunk(16, height, 16); - } - - private void generateInto(int blockX, int blockZ, Hunk blocks, Hunk biomes, - boolean multicore) throws Exception { - if (router == null) { - engine.generate(blockX, blockZ, blocks, biomes, multicore); - return; - } - try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = router.openCoordinateScope(blockX, blockZ); - GenerationSessionLease lease = engine.acquireGenerationLease("probe_terrain_stage"); - IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { - engine.generate(blockX, blockZ, blocks, biomes, multicore); - historyScope.claimGeneratedSemantics(); - } - } - - private static NativeBiome plainsBiome() { - return IrisPlatforms.get().registries().biome("minecraft:plains"); - } - } - private static final class OutcomeSink implements AutoCloseable { private final MessageDigest signature = sha256(); private final List measuredTimings; diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java new file mode 100644 index 000000000..efbb08746 --- /dev/null +++ b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.probe; + +import art.arcane.iris.generation.context.IrisContext; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationSessionLease; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.volmlib.nativelib.terrain.NativeBiome; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.hunk.Hunk; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +final class ProbeChunkGenerator { + private final Engine engine; + private final GenerationHistoryRuntimeRouter router; + private final boolean productionBuffers; + private final AtomicInteger expandedBiomeHunks = new AtomicInteger(); + + ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) { + this.engine = engine; + this.router = router; + this.productionBuffers = productionBuffers; + } + + Engine engine() { + return engine; + } + + int expandedBiomeHunks() { + return expandedBiomeHunks.get(); + } + + GenerationProbe.ChunkOutcome generate(int index, GenerationProbe.ChunkCoordinate coordinate, boolean multicore) { + int height = engine.getTarget().getHeight(); + List failures = new ArrayList<>(); + Hunk blocks = newBlocks(height); + Hunk biomes = newBiomes(height); + long started = System.nanoTime(); + try { + generateInto(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, multicore); + } catch (Throwable e) { + failures.add(e); + } + long elapsed = System.nanoTime() - started; + if (biomes instanceof ProductionBiomeHunk production && production.expanded()) { + expandedBiomeHunks.incrementAndGet(); + } + return new GenerationProbe.ChunkOutcome(index, coordinate, elapsed, blocks, biomes, failures); + } + + private Hunk newBlocks(int height) { + return productionBuffers ? new ProductionBlockHunk(height) : Hunk.newArrayHunk(16, height, 16); + } + + private Hunk newBiomes(int height) { + return productionBuffers ? new ProductionBiomeHunk(height, ProbeChunkGenerator::plainsBiome) + : Hunk.newArrayHunk(16, height, 16); + } + + private void generateInto(int blockX, int blockZ, Hunk blocks, Hunk biomes, + boolean multicore) throws Exception { + if (router == null) { + engine.generate(blockX, blockZ, blocks, biomes, multicore); + return; + } + try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = router.openCoordinateScope(blockX, blockZ); + GenerationSessionLease lease = engine.acquireGenerationLease("probe_terrain_stage"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + engine.generate(blockX, blockZ, blocks, biomes, multicore); + historyScope.claimGeneratedSemantics(); + } + } + + private static NativeBiome plainsBiome() { + return IrisPlatforms.get().registries().biome("minecraft:plains"); + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeHydrologyPreplan.java b/probe/src/main/java/art/arcane/iris/probe/ProbeHydrologyPreplan.java new file mode 100644 index 000000000..3df8d2bd2 --- /dev/null +++ b/probe/src/main/java/art/arcane/iris/probe/ProbeHydrologyPreplan.java @@ -0,0 +1,109 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.probe; + +import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings; +import art.arcane.iris.generation.hydrology.HydrologyTileCache; +import art.arcane.iris.generation.hydrology.HydrologyTileKey; +import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; +import art.arcane.iris.generation.runtime.Engine; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +record ProbeHydrologyPreplan(int tiles, int tileSize, int publicationRadius, int reachChunks, long elapsedNanos) { + private static final int PLANNING_THREADS = 4; + + static Optional run(Engine engine, GenerationProbe.ProbeConfiguration configuration) + throws Exception { + IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime(); + if (hydrology == null) { + return Optional.empty(); + } + HydrologyPlannerSettings settings = hydrology.settings(); + int tileSize = settings.routing().tileSize(); + int publicationRadius = settings.publicationRadius(); + int reachChunks = Math.max(engine.getMantle().getRadius(), engine.getMantle().getRealRadius()); + List keys = tiles(configuration, tileSize, publicationRadius, reachChunks); + long started = System.nanoTime(); + ExecutorService executor = Executors.newFixedThreadPool(Math.min(keys.size(), PLANNING_THREADS)); + try { + List> plans = new ArrayList<>(keys.size()); + for (HydrologyTileKey key : keys) { + plans.add(executor.submit(() -> hydrology.tile(key))); + } + for (Future plan : plans) { + await(plan); + } + } finally { + executor.shutdown(); + } + return Optional.of(new ProbeHydrologyPreplan(keys.size(), tileSize, publicationRadius, reachChunks, + System.nanoTime() - started)); + } + + static List tiles(GenerationProbe.ProbeConfiguration configuration, int tileSize, + int publicationRadius, int reachChunks) { + HydrologyTileCache.PregenerationArea area = GenerationProbe.pregenerationArea(configuration); + long reach = (long) reachChunks * 16L + publicationRadius; + int minimumX = Math.toIntExact(Math.floorDiv(area.minimumBlockX() - reach, tileSize)) - 1; + int minimumZ = Math.toIntExact(Math.floorDiv(area.minimumBlockZ() - reach, tileSize)) - 1; + int maximumX = Math.toIntExact(Math.floorDiv(area.maximumBlockX() + reach, tileSize)) + 1; + int maximumZ = Math.toIntExact(Math.floorDiv(area.maximumBlockZ() + reach, tileSize)) + 1; + int centerX = Math.floorDiv(area.centerBlockX(), tileSize); + int centerZ = Math.floorDiv(area.centerBlockZ(), tileSize); + List keys = new ArrayList<>((maximumX - minimumX + 1) * (maximumZ - minimumZ + 1)); + for (int tileZ = minimumZ; tileZ <= maximumZ; tileZ++) { + for (int tileX = minimumX; tileX <= maximumX; tileX++) { + keys.add(new HydrologyTileKey(tileX, tileZ)); + } + } + keys.sort(Comparator.comparingInt((HydrologyTileKey key) -> + Math.max(Math.abs(key.tileX() - centerX), Math.abs(key.tileZ() - centerZ)))); + return keys; + } + + String machineLine() { + return String.format(Locale.ROOT, + "IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=%d tile_size=%d publication_radius=%d reach_chunks=%d planned_ms=%.3f", + tiles, tileSize, publicationRadius, reachChunks, elapsedNanos / 1_000_000D); + } + + private static void await(Future plan) throws Exception { + try { + plan.get(); + } catch (ExecutionException failure) { + Throwable cause = failure.getCause(); + if (cause instanceof Error error) { + throw error; + } + if (cause instanceof Exception exception) { + throw exception; + } + throw new IllegalStateException(cause); + } + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeWindowMetrics.java b/probe/src/main/java/art/arcane/iris/probe/ProbeWindowMetrics.java new file mode 100644 index 000000000..bc5872682 --- /dev/null +++ b/probe/src/main/java/art/arcane/iris/probe/ProbeWindowMetrics.java @@ -0,0 +1,67 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.probe; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.EngineMetrics; +import art.arcane.iris.generation.runtime.GenerationCounter; +import art.arcane.volmlib.util.cache.CacheContention; + +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +record ProbeWindowMetrics(Map stages, Map counters) { + static ProbeWindowMetrics capture(Engine engine, boolean counters) { + return new ProbeWindowMetrics(engine.getMetrics().stageTotals(), counters ? counterValues() : Map.of()); + } + + static String stagesLine(ProbeWindowMetrics start, ProbeWindowMetrics end, int chunks, int threads) { + StringBuilder line = new StringBuilder(512); + line.append("IRIS_GENPROBE_STAGES version=1 chunks=").append(chunks).append(" threads=").append(threads); + for (Map.Entry entry : end.stages().entrySet()) { + EngineMetrics.StageTotal earlier = start.stages().get(entry.getKey()); + EngineMetrics.StageTotal window = earlier == null ? entry.getValue() : entry.getValue().since(earlier); + line.append(' ').append(entry.getKey()).append('=') + .append(String.format(Locale.ROOT, "%.3f", window.millis())); + } + return line.toString(); + } + + static String countersLine(ProbeWindowMetrics start, ProbeWindowMetrics end, int chunks) { + StringBuilder line = new StringBuilder(256); + line.append("IRIS_GENPROBE_COUNTERS version=1 chunks=").append(chunks); + for (Map.Entry entry : end.counters().entrySet()) { + long window = entry.getValue() - start.counters().getOrDefault(entry.getKey(), 0L); + line.append(' ').append(entry.getKey()).append('=').append(window) + .append(' ').append(entry.getKey()).append("_per_chunk=") + .append(String.format(Locale.ROOT, "%.1f", chunks == 0 ? 0D : window / (double) chunks)); + } + return line.toString(); + } + + private static Map counterValues() { + Map values = new LinkedHashMap<>(); + for (GenerationCounter counter : GenerationCounter.values()) { + values.put(counter.key(), counter.sum()); + } + values.put("cache_duplicate_computations", CacheContention.duplicateComputations()); + return values; + } +} diff --git a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java index 1bbc84ab2..54d240378 100644 --- a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java +++ b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java @@ -171,8 +171,8 @@ public final class GenerationProbeTest { after.put("total", new EngineMetrics.StageTotal(350.25D, 20L)); after.put("context.prefill.height", new EngineMetrics.StageTotal(12.5D, 10L)); - String line = GenerationProbe.stagesLine(new GenerationProbe.WindowMetrics(before, Map.of()), - new GenerationProbe.WindowMetrics(after, Map.of()), 10, 1); + String line = ProbeWindowMetrics.stagesLine(new ProbeWindowMetrics(before, Map.of()), + new ProbeWindowMetrics(after, Map.of()), 10, 1); assertEquals("IRIS_GENPROBE_STAGES version=1 chunks=10 threads=1 total=250.250 context.prefill.height=12.500", line); } @@ -186,8 +186,8 @@ public final class GenerationProbeTest { after.put("generator_evaluations", 5_000L); after.put("corner_starcasts", 330L); - String line = GenerationProbe.countersLine(new GenerationProbe.WindowMetrics(Map.of(), before), - new GenerationProbe.WindowMetrics(Map.of(), after), 2); + String line = ProbeWindowMetrics.countersLine(new ProbeWindowMetrics(Map.of(), before), + new ProbeWindowMetrics(Map.of(), after), 2); assertEquals("IRIS_GENPROBE_COUNTERS version=1 chunks=2 generator_evaluations=4000" + " generator_evaluations_per_chunk=2000.0 corner_starcasts=330 corner_starcasts_per_chunk=165.0", line); @@ -200,7 +200,7 @@ public final class GenerationProbeTest { int tileSize = 512; int publicationRadius = 96; int reachChunks = 13; - List keys = GenerationProbe.preplanTiles(configuration, tileSize, publicationRadius, reachChunks); + List keys = ProbeHydrologyPreplan.tiles(configuration, tileSize, publicationRadius, reachChunks); Set planned = new HashSet<>(keys); assertEquals(keys.size(), planned.size());