mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
probe-
This commit is contained in:
+21
-7
@@ -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<String> probePack = providers.gradleProperty('probePack')
|
||||
Provider<String> probeDimension = providers.gradleProperty('probeDimension')
|
||||
Provider<String> probeWarmupChunks = providers.gradleProperty('probeWarmupChunks').orElse('256')
|
||||
Provider<String> probeMeasuredChunks = providers.gradleProperty('probeMeasuredChunks').orElse('1024')
|
||||
Provider<String> probeCenterChunkX = providers.gradleProperty('probeCenterChunkX').orElse('2048')
|
||||
Provider<String> 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=<dimension-key>')
|
||||
}
|
||||
args(
|
||||
probePack.get(),
|
||||
probeDimension.get(),
|
||||
probeWarmupChunks.get(),
|
||||
probeMeasuredChunks.get(),
|
||||
probeCenterChunkX.get(),
|
||||
probeCenterChunkZ.get())
|
||||
}
|
||||
}
|
||||
|
||||
String deserializationPack = providers.gradleProperty('deserializationPack')
|
||||
|
||||
@@ -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<Throwable> 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: <pack> <dimension> <warmupChunks> <measuredChunks> <centerChunkX> <centerChunkZ>");
|
||||
}
|
||||
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<Long> samples) {
|
||||
if (samples.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one timing sample is required.");
|
||||
}
|
||||
List<Long> 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<ChunkCoordinate> 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<ChunkCoordinate> 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<Throwable> 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<Throwable> 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<String, Integer> distinct = new LinkedHashMap<>();
|
||||
Map<String, String> 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<Throwable> failures = new ArrayList<>();
|
||||
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
|
||||
Hunk<PlatformBiome> 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<String, Integer> 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<ChunkCoordinate> coordinates = scheduleCoordinates(
|
||||
totalChunks, configuration.centerChunkX(), configuration.centerChunkZ());
|
||||
List<Long> measuredTimings = new ArrayList<>(configuration.measuredChunks());
|
||||
MessageDigest signature = sha256();
|
||||
int successfulChunks = 0;
|
||||
int failedChunks = 0;
|
||||
long firstChunkNanos = 0L;
|
||||
Map<String, Integer> distinctFailures = new LinkedHashMap<>();
|
||||
Map<String, String> firstFailureChunk = new LinkedHashMap<>();
|
||||
int height = engine.getTarget().getHeight();
|
||||
|
||||
for (int index = 0; index < totalChunks; index++) {
|
||||
ChunkCoordinate coordinate = coordinates.get(index);
|
||||
drainReported();
|
||||
List<Throwable> failures = new ArrayList<>();
|
||||
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
|
||||
Hunk<PlatformBiome> 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<String, Integer> 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<Throwable> failures,
|
||||
Map<String, Integer> distinctFailures,
|
||||
Map<String, String> 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<PlatformBlockState> blocks, Hunk<PlatformBiome> 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<Path> paths;
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
paths = walk.sorted(Comparator.reverseOrder()).toList();
|
||||
}
|
||||
for (Path path : paths) {
|
||||
Files.deleteIfExists(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Throwable> settleAndDrain() throws InterruptedException {
|
||||
List<Throwable> drained = new ArrayList<>();
|
||||
long quietSince = System.currentTimeMillis();
|
||||
@@ -330,31 +614,6 @@ public final class GenerationProbe {
|
||||
return trace.length > 0 ? trace[0].toString() : "<no frames>";
|
||||
}
|
||||
|
||||
private static String hashChunk(Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, StubBlockState> 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
|
||||
|
||||
@@ -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<GenerationProbe.ChunkCoordinate> coordinates = GenerationProbe.scheduleCoordinates(1280, 2048, -2048);
|
||||
Set<GenerationProbe.ChunkCoordinate> 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user