diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java index e7f1556d8..b8202333e 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java @@ -81,7 +81,7 @@ final class EngineHotloader { previousGeneration.transitionPlan()); PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture( assembly.target, assembly.runtimeKernel, assembly.transitionPlan, engine.isStudio()); - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); EngineRuntime next; try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan, false); @@ -97,7 +97,7 @@ final class EngineHotloader { biomeMaxes); next = previous.withGeneration(nextGeneration); } finally { - engine.runtimeAssembly.remove(); + engine.threadState.setAssembly(null); } Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close); if (retirementFailure != null) { diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java index 71ec16139..79fe292cd 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java @@ -93,7 +93,7 @@ final class EngineRuntimeBuilder { mantleStorageDirectory, selectRuntimeKernel(kernelVersion), transitionPlan); - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { IrisLogging.debug("Setup Engine " + assembly.cacheId); long started = M.ms(); @@ -148,7 +148,7 @@ final class EngineRuntimeBuilder { } throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e); } finally { - engine.runtimeAssembly.remove(); + engine.threadState.setAssembly(null); } } @@ -174,7 +174,7 @@ final class EngineRuntimeBuilder { transitionPlan); PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture( runtimeTarget, assembly.runtimeKernel, transitionPlan, engine.isStudio()); - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId); assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory); @@ -201,7 +201,7 @@ final class EngineRuntimeBuilder { } throw new IllegalStateException("Failed to build a detached Iris generation runtime.", e); } finally { - engine.runtimeAssembly.remove(); + engine.threadState.setAssembly(null); } } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java index 76300e75d..b995fde29 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java @@ -264,20 +264,16 @@ final class EngineShutdownSequence { return failure; } incompleteAssemblies.add(assembly); - RuntimeAssembly previous = engine == null ? null : engine.runtimeAssembly.get(); + RuntimeAssembly previous = engine == null ? null : engine.threadState.assembly(); if (engine != null) { - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); } Throwable assemblyFailure; try { assemblyFailure = closeAssemblyResources(assembly); } finally { if (engine != null) { - if (previous == null) { - engine.runtimeAssembly.remove(); - } else { - engine.runtimeAssembly.set(previous); - } + engine.threadState.setAssembly(previous); } } if (assemblyFailure == null) { @@ -401,7 +397,7 @@ final class EngineShutdownSequence { engine, generationRuntime); Throwable runtimeFailure; - try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) { + try (IrisEngine.GenerationRuntimeScope ignored = engine.threadState.open(binding)) { runtimeFailure = closeGenerationRuntime(generationRuntime, null); } catch (Throwable scopeFailure) { runtimeFailure = scopeFailure; diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineThreadState.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineThreadState.java new file mode 100644 index 000000000..feb15934c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineThreadState.java @@ -0,0 +1,126 @@ +/* + * 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.generation.runtime; + +import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.generation.runtime.IrisEngine.BiomeEnvironmentBinding; +import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeBinding; +import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeScope; + +/** + * Every per-thread engine binding (runtime assembly, biome environment, generation runtime scope) in one frame, + * so the engine getters on generation hot paths pay a single ThreadLocal lookup. + */ +final class EngineThreadState { + private final ThreadLocal frames = new ThreadLocal<>(); + + Frame current() { + return frames.get(); + } + + GenerationRuntimeBinding binding() { + Frame frame = frames.get(); + return frame == null ? null : frame.binding; + } + + RuntimeAssembly assembly() { + Frame frame = frames.get(); + return frame == null ? null : frame.assembly; + } + + void setAssembly(RuntimeAssembly assembly) { + if (assembly != null) { + frame().assembly = assembly; + return; + } + Frame frame = frames.get(); + if (frame != null) { + frame.assembly = null; + releaseIfEmpty(frame); + } + } + + BiomeEnvironmentBinding environment() { + Frame frame = frames.get(); + return frame == null ? null : frame.environment; + } + + void setEnvironment(BiomeEnvironmentBinding environment) { + if (environment != null) { + frame().environment = environment; + return; + } + Frame frame = frames.get(); + if (frame != null) { + frame.environment = null; + releaseIfEmpty(frame); + } + } + + GenerationRuntimeScope open(GenerationRuntimeBinding binding) { + Thread owner = Thread.currentThread(); + Frame frame = frame(); + GenerationRuntimeBinding previous = frame.binding; + frame.binding = binding; + return new GenerationRuntimeScope(this, owner, previous, binding); + } + + void close( + Thread owner, + GenerationRuntimeBinding previous, + GenerationRuntimeBinding installed + ) { + if (Thread.currentThread() != owner) { + throw new IllegalStateException("Iris generation runtime scope closed from a different thread."); + } + Frame frame = frames.get(); + if ((frame == null ? null : frame.binding) != installed) { + throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order."); + } + if (previous != null) { + frame().binding = previous; + return; + } + if (frame != null) { + frame.binding = null; + releaseIfEmpty(frame); + } + } + + private Frame frame() { + Frame frame = frames.get(); + if (frame == null) { + frame = new Frame(); + frames.set(frame); + } + return frame; + } + + private void releaseIfEmpty(Frame frame) { + if (frame.assembly == null && frame.environment == null && frame.binding == null) { + frames.remove(); + } + } + + static final class Frame { + RuntimeAssembly assembly; + BiomeEnvironmentBinding environment; + GenerationRuntimeBinding binding; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/GenerationRuntimeScopeState.java b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationRuntimeScopeState.java deleted file mode 100644 index 38c8efc4d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/runtime/GenerationRuntimeScopeState.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.generation.runtime; - -import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeBinding; -import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeScope; - -final class GenerationRuntimeScopeState { - private final ThreadLocal current = new ThreadLocal<>(); - - GenerationRuntimeBinding current() { - return current.get(); - } - - GenerationRuntimeScope open(GenerationRuntimeBinding binding) { - Thread owner = Thread.currentThread(); - GenerationRuntimeBinding previous = current.get(); - current.set(binding); - return new GenerationRuntimeScope(this, owner, previous, binding); - } - - void close( - Thread owner, - GenerationRuntimeBinding previous, - GenerationRuntimeBinding installed - ) { - if (Thread.currentThread() != owner) { - throw new IllegalStateException("Iris generation runtime scope closed from a different thread."); - } - if (current.get() != installed) { - throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order."); - } - if (previous == null) { - current.remove(); - return; - } - current.set(previous); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index 6998949ae..d00da2e17 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -117,13 +117,7 @@ public class IrisEngine implements Engine { final Object generationHistoryRuntimeRouterLock = new Object(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - final ThreadLocal runtimeAssembly = new ThreadLocal<>(); - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - final ThreadLocal biomeEnvironmentScopes = new ThreadLocal<>(); - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - final GenerationRuntimeScopeState generationRuntimeScopes = new GenerationRuntimeScopeState(); + final EngineThreadState threadState = new EngineThreadState(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) final Set detachedGenerationRuntimes = Collections.synchronizedSet( @@ -383,7 +377,7 @@ public class IrisEngine implements Engine { throw new IllegalStateException("World entry hydrology preparation could not acquire its generation session.", failure); } try (lease; - GenerationRuntimeScope runtimeScope = generationRuntimeScopes.open(binding); + GenerationRuntimeScope runtimeScope = threadState.open(binding); IrisContext.Scope context = IrisContext.open(this, lease.sessionId(), null)) { getComplex().getHydrologyRuntime().prepareChunkColumns(blockX, blockZ); } catch (Exception failure) { @@ -573,8 +567,8 @@ public class IrisEngine implements Engine { @Override public BiomeEnvironment.Scope openBiomeEnvironmentScope(BiomeEnvironment environment) { BiomeEnvironmentBinding binding = new BiomeEnvironmentBinding( - Objects.requireNonNull(environment, "environment"), biomeEnvironmentScopes.get()); - biomeEnvironmentScopes.set(binding); + Objects.requireNonNull(environment, "environment"), threadState.environment()); + threadState.setEnvironment(binding); return binding; } @@ -980,7 +974,7 @@ public class IrisEngine implements Engine { } public boolean hasGenerationRuntimeScope() { - return generationRuntimeScopes.current() != null; + return threadState.binding() != null; } private boolean usesScopedNaturalTerrain() { @@ -1059,7 +1053,7 @@ public class IrisEngine implements Engine { } public GenerationRuntimeBinding captureGenerationRuntimeBinding() { - GenerationRuntimeBinding scoped = generationRuntimeScopes.current(); + GenerationRuntimeBinding scoped = threadState.binding(); return scoped == null ? getActiveGenerationRuntimeBinding() : scoped; } @@ -1075,7 +1069,7 @@ public class IrisEngine implements Engine { if (!isGenerationRuntimeBindingLive(required)) { throw new IllegalStateException("Iris generation runtime binding is closed or no longer owned."); } - return generationRuntimeScopes.open(required); + return threadState.open(required); } } @@ -1266,75 +1260,85 @@ public class IrisEngine implements Engine { @Override public SeedManager getSeedManager() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.seedManager; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? seedManager : current.seedManager(); } @Override public IrisComplex getComplex() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null && assembly.complex != null) { return assembly.complex; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.complex(); } @Override public EngineTarget getTarget() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.target; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? publishedTarget : current.target(); } @Override public IrisData getData() { - BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get(); - if (environment != null && !hasGenerationRuntimeScope()) { - return environment.environment.data(); + EngineThreadState.Frame frame = threadState.current(); + if (frame != null) { + BiomeEnvironmentBinding environment = frame.environment; + if (environment != null && frame.binding == null) { + return environment.environment.data(); + } + RuntimeAssembly assembly = frame.assembly; + if (assembly != null) { + return assembly.target.getData(); + } } - RuntimeAssembly assembly = runtimeAssembly.get(); - if (assembly != null) { - return assembly.target.getData(); - } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? publishedTarget.getData() : current.data(); } @Override public IrisDimension getDimension() { - BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get(); - if (environment != null && !hasGenerationRuntimeScope()) { - return environment.environment.dimension(); + EngineThreadState.Frame frame = threadState.current(); + if (frame != null) { + BiomeEnvironmentBinding environment = frame.environment; + if (environment != null && frame.binding == null) { + return environment.environment.dimension(); + } + RuntimeAssembly assembly = frame.assembly; + if (assembly != null) { + return assembly.target.getDimension(); + } } - RuntimeAssembly assembly = runtimeAssembly.get(); - if (assembly != null) { - return assembly.target.getDimension(); - } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? publishedTarget.getDimension() : current.dimension(); } @Override public EngineMantle getMantle() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null && assembly.mantle != null) { return assembly.mantle; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.mantle(); } @Override public EngineMode getMode() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.mode != null) { return assembly.mode; } @@ -1344,7 +1348,7 @@ public class IrisEngine implements Engine { @Override public EngineEffects getEffects() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.effects != null) { return assembly.effects; } @@ -1354,7 +1358,7 @@ public class IrisEngine implements Engine { @Override public EngineWorldManager getWorldManager() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.worldManager != null) { return assembly.worldManager; } @@ -1364,27 +1368,29 @@ public class IrisEngine implements Engine { @Override public UpperDimensionContext getUpperContext() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.upperContext; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.upperContext(); } @Override public DimensionStackContext getDimensionStackContext() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.dimensionStackContext; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.dimensionStackContext(); } @Override public CompletableFuture getHash32() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.hash32 != null) { return assembly.hash32; } @@ -1579,16 +1585,21 @@ public class IrisEngine implements Engine { @Override public int getCacheID() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.cacheId; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? -1 : current.cacheId(); } private GenerationRuntime selectedGenerationRuntime() { - GenerationRuntimeBinding scoped = generationRuntimeScopes.current(); + return selectedGenerationRuntime(threadState.current()); + } + + private GenerationRuntime selectedGenerationRuntime(EngineThreadState.Frame frame) { + GenerationRuntimeBinding scoped = frame == null ? null : frame.binding; if (scoped != null && isGenerationRuntimeBindingLive(scoped)) { return scoped.runtime; } @@ -1597,7 +1608,7 @@ public class IrisEngine implements Engine { } public GenerationKernelRegistry.Version getGenerationKernelVersion() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null) { return assembly.kernelVersion; } @@ -1605,7 +1616,7 @@ public class IrisEngine implements Engine { } public GenerationKernelRegistry.RuntimeKernel getGenerationRuntimeKernel() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null) { return assembly.runtimeKernel; } @@ -1613,7 +1624,7 @@ public class IrisEngine implements Engine { } public TransitionGenerationPlan getTransitionGenerationPlan() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null) { return assembly.transitionPlan; } @@ -1737,14 +1748,14 @@ public class IrisEngine implements Engine { } public static final class GenerationRuntimeScope implements AutoCloseable { - private final GenerationRuntimeScopeState state; + private final EngineThreadState state; private final Thread owner; private final GenerationRuntimeBinding previous; private final GenerationRuntimeBinding installed; private boolean closed; GenerationRuntimeScope( - GenerationRuntimeScopeState state, + EngineThreadState state, Thread owner, GenerationRuntimeBinding previous, GenerationRuntimeBinding installed @@ -1766,7 +1777,7 @@ public class IrisEngine implements Engine { } - private final class BiomeEnvironmentBinding implements BiomeEnvironment.Scope { + final class BiomeEnvironmentBinding implements BiomeEnvironment.Scope { private final BiomeEnvironment environment; private final BiomeEnvironmentBinding previous; private final Thread owner = Thread.currentThread(); @@ -1785,14 +1796,10 @@ public class IrisEngine implements Engine { if (closed) { return; } - if (biomeEnvironmentScopes.get() != this) { + if (threadState.environment() != this) { throw new IllegalStateException("Biome environment scopes must close in reverse order."); } - if (previous == null) { - biomeEnvironmentScopes.remove(); - } else { - biomeEnvironmentScopes.set(previous); - } + threadState.setEnvironment(previous); closed = true; } } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java index 75defd7d6..093f7c1bc 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java @@ -112,7 +112,7 @@ public class EngineShutdownDrainTest { doAnswer(invocation -> { Future continuation = worker.submit(() -> { try (IrisEngine.GenerationRuntimeScope ignored = fixture.engine.openGenerationRuntimeScope(binding)) { - assertSame(binding, fixture.engine.generationRuntimeScopes.current()); + assertSame(binding, fixture.engine.threadState.binding()); assertSame(fixture.runtime, fixture.engine.runtime); assertTrue(fixture.engine.getClosing().get()); assertThrows(IllegalStateException.class, () -> fixture.engine.requireRunning("start generation")); @@ -513,16 +513,16 @@ public class EngineShutdownDrainTest { assembly.mantle = mock(EngineMantle.class); assembly.ownsMantle = true; RuntimeAssembly previous = mock(RuntimeAssembly.class); - fixture.engine.runtimeAssembly.set(previous); + fixture.engine.threadState.setAssembly(previous); IllegalStateException failure = new IllegalStateException("Planner still active"); doAnswer(invocation -> { - assertSame(assembly, fixture.engine.runtimeAssembly.get()); + assertSame(assembly, fixture.engine.threadState.assembly()); throw failure; }).when(assembly.complex).close(); assertSame(failure, fixture.shutdown.closeAssembly(assembly, null)); - assertSame(previous, fixture.engine.runtimeAssembly.get()); - fixture.engine.runtimeAssembly.remove(); + assertSame(previous, fixture.engine.threadState.assembly()); + fixture.engine.threadState.setAssembly(null); assertTrue(fixture.shutdown.retainsData(detachedTarget.getData())); assertTrue(fixture.shutdown.closeDetachedTarget(detachedTarget, null) instanceof IllegalStateException); verify(detachedTarget, never()).close(); @@ -531,13 +531,13 @@ public class EngineShutdownDrainTest { verify(fixture.target, never()).close(); verifyNoInteractions(assembly.mantle); doAnswer(invocation -> { - assertSame(assembly, fixture.engine.runtimeAssembly.get()); + assertSame(assembly, fixture.engine.threadState.assembly()); return null; }).when(assembly.complex).close(); fixture.shutdown.close(); } - assertNull(fixture.engine.runtimeAssembly.get()); + assertNull(fixture.engine.threadState.assembly()); assertFalse(assembly.ownsMantle); assertFalse(fixture.shutdown.retainsData(detachedTarget.getData())); verify(assembly.mantle).close(); @@ -851,10 +851,9 @@ public class EngineShutdownDrainTest { setField(engine, "shutdownSequence", shutdown); setField(engine, "engineDataStore", mock(EngineDataStore.class)); setField(engine, "lifecycleLock", new Object()); - setField(engine, "runtimeAssembly", new ThreadLocal()); + setField(engine, "threadState", new EngineThreadState()); setField(engine, "detachedGenerationRuntimes", ConcurrentHashMap.newKeySet()); setField(engine, "retiringGenerationRuntimes", ConcurrentHashMap.newKeySet()); - setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState()); AtomicBoolean closing = new AtomicBoolean(); setField(engine, "closing", closing); when(engine.getClosing()).thenReturn(closing); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/EngineThreadStateTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/EngineThreadStateTest.java new file mode 100644 index 000000000..a3d7a98ee --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/EngineThreadStateTest.java @@ -0,0 +1,76 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; + +public class EngineThreadStateTest { + @Test + public void bindingsShareOneFrameAndReleaseItWhenAllAreCleared() { + EngineThreadState state = new EngineThreadState(); + RuntimeAssembly assembly = mock(RuntimeAssembly.class); + IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + + assertNull(state.current()); + state.setAssembly(assembly); + IrisEngine.GenerationRuntimeScope scope = state.open(binding); + EngineThreadState.Frame frame = state.current(); + assertSame(assembly, frame.assembly); + assertSame(binding, frame.binding); + + state.setAssembly(null); + assertSame(frame, state.current()); + assertSame(binding, state.binding()); + scope.close(); + assertNull(state.current()); + } + + @Test + public void nestedScopesRestoreThePreviousBindingInOrder() { + EngineThreadState state = new EngineThreadState(); + IrisEngine.GenerationRuntimeBinding outer = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + IrisEngine.GenerationRuntimeBinding inner = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + + IrisEngine.GenerationRuntimeScope outerScope = state.open(outer); + IrisEngine.GenerationRuntimeScope innerScope = state.open(inner); + assertThrows(IllegalStateException.class, outerScope::close); + innerScope.close(); + assertSame(outer, state.binding()); + outerScope.close(); + assertNull(state.current()); + } + + @Test + public void framesAreThreadConfinedAndScopesRejectForeignClose() throws InterruptedException { + EngineThreadState state = new EngineThreadState(); + IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + IrisEngine.GenerationRuntimeScope scope = state.open(binding); + AtomicReference seen = new AtomicReference<>(binding); + AtomicReference foreignClose = new AtomicReference<>(); + Thread other = new Thread(() -> { + seen.set(state.current()); + try { + scope.close(); + } catch (Throwable failure) { + foreignClose.set(failure); + } + }); + other.start(); + other.join(); + + assertNull(seen.get()); + assertSame(IllegalStateException.class, foreignClose.get().getClass()); + scope.close(); + assertNull(state.current()); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java index 3102a557e..53d93e80c 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java @@ -103,7 +103,7 @@ public class IrisCarveWallPaletteScopeTest { IrisContext.Scope context = IrisContext.open(fixture.engine, 71L, fixture.context); MockedStatic terrain = mockStatic(TerrainMatterView.class)) { if (mode == Mode.ASSEMBLY) { - fixture.engine.runtimeAssembly.set(fixture.assembly); + fixture.engine.threadState.setAssembly(fixture.assembly); } terrain.when(() -> TerrainMatterView.iterate(eq(fixture.chunk), eq(MatterCavern.class), any())) .thenAnswer(call -> { @@ -137,7 +137,7 @@ public class IrisCarveWallPaletteScopeTest { verify(fixture.chunk).release(); assertEquals(1, fixture.dataLookups.get()); } finally { - fixture.engine.runtimeAssembly.remove(); + fixture.engine.threadState.setAssembly(null); } } assertSame(fixture.active.data(), fixture.engine.getData()); @@ -199,9 +199,7 @@ public class IrisCarveWallPaletteScopeTest { active = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle); detached = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle); field(engine, IrisEngine.class, "lifecycleLock", new Object()); - field(engine, IrisEngine.class, "runtimeAssembly", new ThreadLocal()); - field(engine, IrisEngine.class, "biomeEnvironmentScopes", new ThreadLocal<>()); - field(engine, IrisEngine.class, "generationRuntimeScopes", new GenerationRuntimeScopeState()); + field(engine, IrisEngine.class, "threadState", new EngineThreadState()); field(engine, IrisEngine.class, "detachedGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>())); field(engine, IrisEngine.class, "retiringGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>())); engine.detachedGenerationRuntimes.add(detached); @@ -255,7 +253,7 @@ public class IrisCarveWallPaletteScopeTest { private IrisBiome scopedBiome(IrisBiome biome) { try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope( new IrisEngine.GenerationRuntimeBinding(engine, active))) { - assertSame(engine.runtimeAssembly.get() == null ? active.data() : assemblyData, engine.getData()); + assertSame(engine.threadState.assembly() == null ? active.data() : assemblyData, engine.getData()); } return biome; } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java index d5dd32cd3..40a8868c8 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java @@ -62,7 +62,7 @@ public class IrisEngineEntryHydrologyTest { doAnswer(invocation -> { assertSame(engine, IrisContext.require().getEngine()); assertEquals(93L, IrisContext.require().getGenerationSessionId()); - assertSame(binding, engine.generationRuntimeScopes.current()); + assertSame(binding, engine.threadState.binding()); return null; }).when(hydrology).prepareChunkColumns(0, 0); AtomicReference> queued = new AtomicReference<>(); @@ -75,7 +75,7 @@ public class IrisEngineEntryHydrologyTest { verify(hydrology).prepareChunkColumns(0, 0); verify(lease).close(); assertNull(IrisContext.get()); - assertNull(engine.generationRuntimeScopes.current()); + assertNull(engine.threadState.binding()); tasks.drainBackgroundTasks("test").requireComplete("test"); } } @@ -325,9 +325,9 @@ public class IrisEngineEntryHydrologyTest { Field closing = IrisEngine.class.getDeclaredField("closing"); closing.setAccessible(true); closing.set(engine, new AtomicBoolean()); - Field scopes = IrisEngine.class.getDeclaredField("generationRuntimeScopes"); + Field scopes = IrisEngine.class.getDeclaredField("threadState"); scopes.setAccessible(true); - scopes.set(engine, new GenerationRuntimeScopeState()); + scopes.set(engine, new EngineThreadState()); Field lifecycle = IrisEngine.class.getDeclaredField("lifecycleLock"); lifecycle.setAccessible(true); lifecycle.set(engine, new Object()); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java index 3faf566bd..011b55946 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java @@ -923,9 +923,7 @@ public class IrisEngineGenerationRuntimeScopeTest { IrisEngine engine = mock(IrisEngine.class, CALLS_REAL_METHODS); setField(engine, "lifecycleLock", new Object()); setField(engine, "generationHistoryRuntimeRouterLock", new Object()); - setField(engine, "runtimeAssembly", new ThreadLocal()); - setField(engine, "biomeEnvironmentScopes", new ThreadLocal()); - setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState()); + setField(engine, "threadState", new EngineThreadState()); Set detached = Collections.synchronizedSet( Collections.newSetFromMap(new IdentityHashMap())); setField(engine, "detachedGenerationRuntimes", detached);