diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenDiagnostics.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenDiagnostics.java new file mode 100644 index 000000000..be5fefe6a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenDiagnostics.java @@ -0,0 +1,43 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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.core.pregenerator; + +import art.arcane.iris.spi.IrisLogging; + +public final class PregenDiagnostics { + private PregenDiagnostics() { + } + + public static void probeFailed(String probe, Throwable failure) { + IrisLogging.debug("Pregen probe " + describeProbe(probe) + " failed: " + describeFailure(failure)); + } + + public static String describeProbe(String probe) { + return probe == null || probe.isBlank() ? "unnamed" : probe; + } + + public static String describeFailure(Throwable failure) { + if (failure == null) { + return "no throwable reported"; + } + + String message = failure.getMessage(); + return message == null ? failure.getClass().getName() : failure.getClass().getName() + ": " + message; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java index 39152f161..2df82795d 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java @@ -170,7 +170,8 @@ public final class PregenMantleBackpressure { try { return cancelled.getAsBoolean(); - } catch (Throwable ignored) { + } catch (Throwable e) { + PregenDiagnostics.probeFailed("pregen cancellation state", e); return false; } } @@ -178,7 +179,8 @@ public final class PregenMantleBackpressure { private Mantle resolveMantle() { try { return mantleSupplier.get(); - } catch (Throwable ignored) { + } catch (Throwable e) { + PregenDiagnostics.probeFailed("mantle handle for backpressure", e); return null; } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index 399eee3d6..811b5f285 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -22,6 +22,7 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.IrisPaperLikeBackendMode; import art.arcane.iris.core.IrisRuntimeSchedulerMode; import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.pregenerator.PregenDiagnostics; import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregenMantleBackpressure; import art.arcane.iris.core.pregenerator.PregeneratorMethod; @@ -212,7 +213,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { if (coreThreads instanceof Thread[] threadsArray) { return threadsArray.length; } - } catch (Throwable ignored) { + } catch (Throwable e) { + PregenDiagnostics.probeFailed("moonrise worker pool size", e); } return -1; @@ -238,7 +240,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { int resolved = radius > 0 ? Math.max(1, (int) Math.ceil(radius / 32.0)) : 2; evictionWindowRegions = resolved; return resolved; - } catch (Throwable ignored) { + } catch (Throwable e) { + PregenDiagnostics.probeFailed("mantle radius for eviction window", e); return 2; } } @@ -431,7 +434,9 @@ public class AsyncPregenMethod implements PregeneratorMethod { private void unloadChunkSafely(int cx, int cz) { try { world.removePluginChunkTicket(cx, cz, BukkitPlatform.plugin()); - } catch (Throwable ignored) { + } catch (Throwable e) { + IrisLogging.reportError("Async pregen could not release the plugin chunk ticket at " + cx + "," + cz + + " in world " + world.getName() + "; the chunk stays pinned in memory.", e); } try { @@ -492,11 +497,12 @@ public class AsyncPregenMethod implements PregeneratorMethod { } private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) { - try { - IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot()); + IrisLogging.reportError("Failed async pregen chunk load at " + x + "," + z + ".", throwable); - IrisLogging.reportError(throwable); + try { + IrisLogging.warn("Async pregen state at the failed chunk " + x + "," + z + ". " + metricsSnapshot()); } catch (Throwable e) { + PregenDiagnostics.probeFailed("pregen metrics snapshot", e); } return null; @@ -738,7 +744,9 @@ public class AsyncPregenMethod implements PregeneratorMethod { if (engine != null) { try { engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned); - } catch (Throwable ignored) { + } catch (Throwable e) { + IrisLogging.reportError("Async pregen mantle cleanup failed at chunk " + x + "," + z + + " in world " + world.getName() + "; tectonic plates for that chunk stay resident.", e); } } } @@ -759,7 +767,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { metricsEngine = resolvedEngine; } return resolvedEngine; - } catch (Throwable ignored) { + } catch (Throwable e) { + PregenDiagnostics.probeFailed("engine access for world " + world.getName(), e); return null; } } @@ -1094,7 +1103,9 @@ public class AsyncPregenMethod implements PregeneratorMethod { requestMonitoredChunkAsync(x, z) .whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable)); return; - } catch (Throwable ignored) { + } catch (Throwable e) { + PregenDiagnostics.probeFailed("folia direct chunk request at " + x + "," + z + + ", falling back to a region task", e); } Runnable regionTask = () -> { diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java index c56602aef..54f24d0ee 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java @@ -20,6 +20,7 @@ package art.arcane.iris.core.pregenerator.methods; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.pregenerator.PregenDiagnostics; import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.tools.IrisToolbelt; @@ -102,7 +103,8 @@ public class MedievalPregenMethod implements PregeneratorMethod { if (cached != null) { cachedEngine = cached; } - } catch (Throwable ignored) { + } catch (Throwable e) { + PregenDiagnostics.probeFailed("engine access for world " + world.getName(), e); } return cached; } diff --git a/core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java b/core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java index de41f882b..07347095a 100644 --- a/core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java +++ b/core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java @@ -3,10 +3,15 @@ package art.arcane.iris.util.simd; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.IrisSettings; +import java.util.concurrent.atomic.AtomicReference; + public final class SimdSupport { private static final String VECTOR_MODULE = "jdk.incubator.vector"; private static final boolean MODULE_PRESENT = ModuleLayer.boot().findModule(VECTOR_MODULE).isPresent(); + private static final AtomicReference KERNEL_FAILURE = new AtomicReference<>(); + private static final AtomicReference NOISE_KERNEL_FAILURE = new AtomicReference<>(); private static final SimdKernels KERNELS = selectKernels(); + private static final NoiseKernels2D NOISE_KERNELS_2D = selectNoiseKernels2D(); private SimdSupport() { } @@ -15,6 +20,10 @@ public final class SimdSupport { return KERNELS; } + public static NoiseKernels2D noiseKernels2D() { + return NOISE_KERNELS_2D; + } + public static boolean isVectorModulePresent() { return MODULE_PRESENT; } @@ -23,13 +32,75 @@ public final class SimdSupport { return !(KERNELS instanceof ScalarSimdKernels); } + public static Throwable kernelInitializationFailure() { + return KERNEL_FAILURE.get(); + } + + public static Throwable noiseKernelInitializationFailure() { + return NOISE_KERNEL_FAILURE.get(); + } + + public static String kernelStatus() { + return kernelStatus(isVectorized(), KERNELS.describe(), MODULE_PRESENT, KERNEL_FAILURE.get(), simdEnabledInSettings()); + } + + public static String noiseKernelStatus() { + return noiseKernelStatus(!(NOISE_KERNELS_2D instanceof ScalarNoiseKernels2D), NOISE_KERNELS_2D.describe(), + MODULE_PRESENT, NOISE_KERNEL_FAILURE.get(), simdEnabledInSettings()); + } + + static String kernelStatus(boolean vectorized, String description, boolean modulePresent, Throwable failure, boolean settingEnabled) { + if (vectorized) { + return "vector kernels enabled (" + description + ")"; + } + + if (!modulePresent) { + return "scalar kernels active; add --add-modules " + VECTOR_MODULE + " to JVM flags to enable vectorized generation kernels"; + } + + if (failure != null) { + return "scalar kernels active; vector kernel initialization failed: " + describe(failure); + } + + if (!settingEnabled) { + return "vector kernels disabled (performance.simdKernels=false)"; + } + + return "scalar kernels active; the Vector API reported no usable vector shape on this CPU"; + } + + static String noiseKernelStatus(boolean vectorized, String description, boolean modulePresent, Throwable failure, boolean settingEnabled) { + if (vectorized) { + return "2D noise vector kernels enabled (" + description + ")"; + } + + if (!modulePresent) { + return "2D noise scalar kernels active; the " + VECTOR_MODULE + " module is not on this JVM"; + } + + if (failure != null) { + return "2D noise scalar kernels active; vector kernel initialization failed: " + describe(failure); + } + + if (!settingEnabled) { + return "2D noise vector kernels disabled (performance.simdKernels=false)"; + } + + return "2D noise scalar kernels active; this CPU has fewer than the 4 double lanes the vector kernel needs to pay for itself"; + } + public static void install() { - if (isVectorized()) { - IrisLogging.info("SIMD: vector kernels enabled (" + KERNELS.describe() + ")"); - } else if (!MODULE_PRESENT) { - IrisLogging.info("SIMD: scalar kernels active; add --add-modules " + VECTOR_MODULE + " to JVM flags to enable vectorized generation kernels"); - } else { - IrisLogging.info("SIMD: vector kernels disabled (performance.simdKernels=false)"); + IrisLogging.info("SIMD: " + kernelStatus()); + IrisLogging.info("SIMD: " + noiseKernelStatus()); + + Throwable failure = KERNEL_FAILURE.get(); + if (failure != null) { + IrisLogging.reportError("Iris could not initialize its Vector API kernels; generation runs on the scalar kernels instead.", failure); + } + + Throwable noiseFailure = NOISE_KERNEL_FAILURE.get(); + if (noiseFailure != null) { + IrisLogging.reportError("Iris could not initialize its Vector API 2D noise kernels; noise runs on the scalar kernels instead.", noiseFailure); } } @@ -41,6 +112,25 @@ public final class SimdSupport { try { return (SimdKernels) Class.forName("art.arcane.iris.util.simd.VectorSimdKernels").getDeclaredConstructor().newInstance(); } catch (Throwable e) { + KERNEL_FAILURE.set(e); + return null; + } + } + + public static NoiseKernels2D createVectorNoiseKernels2D() { + if (!MODULE_PRESENT) { + return null; + } + + try { + Class cls = Class.forName("art.arcane.iris.util.simd.VectorNoiseKernels2D"); + boolean profitable = (boolean) cls.getMethod("profitable").invoke(null); + if (!profitable) { + return null; + } + return (NoiseKernels2D) cls.getDeclaredConstructor().newInstance(); + } catch (Throwable e) { + NOISE_KERNEL_FAILURE.set(e); return null; } } @@ -54,36 +144,25 @@ public final class SimdSupport { return vector == null ? new ScalarSimdKernels() : vector; } - private static final NoiseKernels2D NOISE_KERNELS_2D = selectNoiseKernels2D(); - - public static NoiseKernels2D noiseKernels2D() { - return NOISE_KERNELS_2D; - } - - public static NoiseKernels2D createVectorNoiseKernels2D() { - if (!MODULE_PRESENT) { - return null; - } - try { - Class cls = Class.forName("art.arcane.iris.util.simd.VectorNoiseKernels2D"); - boolean profitable = (boolean) cls.getMethod("profitable").invoke(null); - if (!profitable) { - return null; - } - return (NoiseKernels2D) cls.getDeclaredConstructor().newInstance(); - } catch (Throwable e) { - return null; - } - } - private static NoiseKernels2D selectNoiseKernels2D() { if (!simdEnabledInSettings()) { return new ScalarNoiseKernels2D(); } + NoiseKernels2D vector = createVectorNoiseKernels2D(); return vector == null ? new ScalarNoiseKernels2D() : vector; } + private static String describe(Throwable failure) { + Throwable root = failure; + while (root.getCause() != null && root.getCause() != root) { + root = root.getCause(); + } + + String message = root.getMessage(); + return message == null ? root.getClass().getName() : root.getClass().getName() + ": " + message; + } + private static boolean simdEnabledInSettings() { try { return IrisSettings.get().getPerformance().isSimdKernels(); diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenDiagnosticsTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenDiagnosticsTest.java new file mode 100644 index 000000000..e2805bb57 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenDiagnosticsTest.java @@ -0,0 +1,84 @@ +package art.arcane.iris.core.pregenerator; + +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.LogLevel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class PregenDiagnosticsTest { + private final List emitted = new ArrayList<>(); + private final List levels = new ArrayList<>(); + private IrisPlatform previousPlatform; + + @Before + public void captureLog() { + previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatforms.unbind(); + emitted.clear(); + levels.clear(); + IrisPlatform platform = mock(IrisPlatform.class); + doAnswer(invocation -> { + levels.add(invocation.getArgument(0, LogLevel.class)); + emitted.add(invocation.getArgument(1, String.class)); + return null; + }).when(platform).log(ArgumentMatchers.any(LogLevel.class), ArgumentMatchers.anyString()); + IrisPlatforms.bind(platform); + } + + @After + public void restorePlatform() { + IrisPlatforms.unbind(); + if (previousPlatform != null) { + IrisPlatforms.bind(previousPlatform); + } + } + + @Test + public void aFailedProbeReportsItsNameClassAndMessageAtDebug() { + PregenDiagnostics.probeFailed("moonrise worker pool size", new IllegalStateException("no such field")); + + assertEquals(1, emitted.size()); + assertEquals(LogLevel.DEBUG, levels.get(0)); + assertTrue(emitted.get(0), emitted.get(0).contains("moonrise worker pool size")); + assertTrue(emitted.get(0), emitted.get(0).contains(IllegalStateException.class.getName())); + assertTrue(emitted.get(0), emitted.get(0).contains("no such field")); + } + + @Test + public void aThrowableWithNoMessageStillReportsItsClass() { + PregenDiagnostics.probeFailed("engine access", new NoClassDefFoundError()); + + assertEquals(1, emitted.size()); + assertTrue(emitted.get(0), emitted.get(0).contains(NoClassDefFoundError.class.getName())); + assertTrue(emitted.get(0), emitted.get(0).contains("engine access")); + } + + @Test + public void aMissingProbeNameAndThrowableStillProduceAReadableLine() { + PregenDiagnostics.probeFailed(null, null); + + assertEquals(1, emitted.size()); + assertTrue(emitted.get(0), emitted.get(0).contains("unnamed")); + assertTrue(emitted.get(0), emitted.get(0).contains("no throwable reported")); + } + + @Test + public void percentSignsInAProbeNameAreNotTreatedAsFormatSpecifiers() { + PregenDiagnostics.probeFailed("heap at 95% headroom", new IllegalArgumentException("bad %s value")); + + assertEquals(1, emitted.size()); + assertTrue(emitted.get(0), emitted.get(0).contains("heap at 95% headroom")); + assertTrue(emitted.get(0), emitted.get(0).contains("bad %s value")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenSilentFailureContractTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenSilentFailureContractTest.java new file mode 100644 index 000000000..495ae768f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenSilentFailureContractTest.java @@ -0,0 +1,136 @@ +package art.arcane.iris.core.pregenerator; + +import org.junit.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PregenSilentFailureContractTest { + @Test + public void noPregenSourceSwallowsAThrowableWithoutSayingAnything() throws IOException { + List offenders = new ArrayList<>(); + Path pregenRoot = sourceRoot().resolve("art/arcane/iris/core/pregenerator"); + try (Stream sources = Files.walk(pregenRoot)) { + for (Path source : sources.filter(path -> path.toString().endsWith(".java")).toList()) { + String text = Files.readString(source); + if (text.contains("catch (Throwable ignored)") || text.contains("catch (Exception ignored)")) { + offenders.add(source.getFileName().toString()); + } + } + } + + assertEquals("silent catches on the pregen path: " + offenders, List.of(), offenders); + } + + @Test + public void mantleCleanupFailureIsReportedWithItsStackTrace() throws IOException { + String cleanup = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"), + "private void cleanupMantleChunksCoveredBy("); + + assertTrue(cleanup, cleanup.contains("IrisLogging.reportError(")); + assertTrue(cleanup, cleanup.contains("catch (Throwable e)")); + } + + @Test + public void aStrandedPluginChunkTicketIsReportedWithItsStackTrace() throws IOException { + String unload = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"), + "private void unloadChunkSafely("); + + assertTrue(unload, unload.contains("removePluginChunkTicket")); + assertEquals(unload, 2, countOccurrences(unload, "IrisLogging.reportError(")); + } + + @Test + public void capabilityProbesLogThroughTheNamedDebugHelperRatherThanReportingErrors() throws IOException { + String async = read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"); + + assertTrue(method(async, "private int resolveWorkerPoolThreads()").contains("PregenDiagnostics.probeFailed(")); + assertTrue(method(async, "private int evictionWindow()").contains("PregenDiagnostics.probeFailed(")); + assertTrue(method(async, "private Engine resolveMetricsEngine()").contains("PregenDiagnostics.probeFailed(")); + } + + @Test + public void theFoliaSubmitFallbackNamesTheFailureItIsFallingBackFrom() throws IOException { + String executor = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"), + "private class FoliaRegionExecutor"); + + assertTrue(executor, executor.contains("PregenDiagnostics.probeFailed(")); + assertTrue(executor, executor.contains("falling back to a region task")); + } + + @Test + public void backpressureProbesAnnounceWhyTheyFellBack() throws IOException { + String backpressure = read("art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java"); + + assertTrue(method(backpressure, "private boolean isCancelled()").contains("PregenDiagnostics.probeFailed(")); + assertTrue(method(backpressure, "private Mantle resolveMantle()").contains("PregenDiagnostics.probeFailed(")); + } + + @Test + public void medievalEngineResolutionAnnouncesWhyItFellBack() throws IOException { + String medieval = read("art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java"); + + assertTrue(method(medieval, "private Engine resolveEngine()").contains("PregenDiagnostics.probeFailed(")); + } + + private static int countOccurrences(String text, String needle) { + int count = 0; + int index = text.indexOf(needle); + while (index >= 0) { + count++; + index = text.indexOf(needle, index + needle.length()); + } + return count; + } + + private static Path sourceRoot() throws IOException { + Path current; + try { + current = Path.of(PregenSilentFailureContractTest.class.getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI()); + } catch (URISyntaxException e) { + throw new IOException("Unable to resolve the core source root", e); + } + while (current != null && !Files.isDirectory(current.resolve("src/main/java"))) { + current = current.getParent(); + } + if (current == null) { + throw new IOException("Unable to locate the core source root"); + } + return current.resolve("src/main/java"); + } + + private static String read(String relativePath) throws IOException { + return Files.readString(sourceRoot().resolve(relativePath)); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue("Missing source contract signature: " + signature, start >= 0); + int openBrace = source.indexOf('{', start); + assertTrue("Missing source contract method body: " + signature, openBrace >= 0); + int depth = 0; + for (int index = openBrace; index < source.length(); index++) { + char current = source.charAt(index); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return source.substring(start, index + 1); + } + } + } + throw new IllegalArgumentException("Unclosed source contract method: " + signature); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/simd/SimdSupportStatusTest.java b/core/src/test/java/art/arcane/iris/util/simd/SimdSupportStatusTest.java new file mode 100644 index 000000000..20a0cc2c7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/simd/SimdSupportStatusTest.java @@ -0,0 +1,116 @@ +package art.arcane.iris.util.simd; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.LogLevel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class SimdSupportStatusTest { + private final List emitted = new ArrayList<>(); + private IrisPlatform previousPlatform; + private IrisSettings previousSettings; + + @Before + public void captureLog() { + previousSettings = IrisSettings.settings; + IrisSettings.settings = new IrisSettings(); + previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatforms.unbind(); + emitted.clear(); + IrisPlatform platform = mock(IrisPlatform.class); + doAnswer(invocation -> { + emitted.add(invocation.getArgument(1, String.class)); + return null; + }).when(platform).log(ArgumentMatchers.any(LogLevel.class), ArgumentMatchers.anyString()); + IrisPlatforms.bind(platform); + } + + @After + public void restorePlatform() { + IrisPlatforms.unbind(); + if (previousPlatform != null) { + IrisPlatforms.bind(previousPlatform); + } + IrisSettings.settings = previousSettings; + } + + @Test + public void anInitializationFailureIsNamedInsteadOfBlamingTheSetting() { + String status = SimdSupport.kernelStatus(false, "scalar", true, + new ExceptionInInitializerError(new UnsupportedOperationException("no vector shape")), true); + + assertFalse(status, status.contains("performance.simdKernels=false")); + assertTrue(status, status.contains(UnsupportedOperationException.class.getName())); + assertTrue(status, status.contains("no vector shape")); + } + + @Test + public void anInitializationFailureOutranksADisabledSettingBecauseADisabledSettingNeverAttempts() { + String status = SimdSupport.kernelStatus(false, "scalar", true, new LinkageError("bad class"), false); + + assertTrue(status, status.contains("bad class")); + assertFalse(status, status.contains("performance.simdKernels=false")); + } + + @Test + public void aDisabledSettingIsReportedOnlyWhenNothingFailed() { + String status = SimdSupport.kernelStatus(false, "scalar", true, null, false); + + assertTrue(status, status.contains("performance.simdKernels=false")); + } + + @Test + public void anAbsentVectorModuleIsReportedWithTheJvmFlagThatFixesIt() { + String status = SimdSupport.kernelStatus(false, "scalar", false, null, true); + + assertTrue(status, status.contains("--add-modules jdk.incubator.vector")); + assertFalse(status, status.contains("performance.simdKernels=false")); + } + + @Test + public void anEnabledVectorKernelReportsItsShapeAndNoRemedy() { + String status = SimdSupport.kernelStatus(true, "4x64-bit lanes, S_256_BIT", true, null, true); + + assertTrue(status, status.contains("4x64-bit lanes, S_256_BIT")); + assertFalse(status, status.contains("scalar kernels active")); + } + + @Test + public void aNarrowCpuIsReportedAsUnprofitableRatherThanDisabled() { + String status = SimdSupport.noiseKernelStatus(false, "scalar", true, null, true); + + assertTrue(status, status.contains("4 double lanes")); + assertFalse(status, status.contains("performance.simdKernels=false")); + } + + @Test + public void installStatesTheLiveKernelSelectionForBothKernelSets() { + SimdSupport.install(); + + assertTrue(emitted.toString(), emitted.size() >= 2); + assertTrue(emitted.toString(), emitted.contains("SIMD: " + SimdSupport.kernelStatus())); + assertTrue(emitted.toString(), emitted.contains("SIMD: " + SimdSupport.noiseKernelStatus())); + } + + @Test + public void theLiveStatusNeverBlamesTheSettingWhileSimdKernelsAreEnabled() { + if (!IrisSettings.get().getPerformance().isSimdKernels()) { + return; + } + + assertFalse(SimdSupport.kernelStatus(), SimdSupport.kernelStatus().contains("performance.simdKernels=false")); + assertFalse(SimdSupport.noiseKernelStatus(), SimdSupport.noiseKernelStatus().contains("performance.simdKernels=false")); + } +}