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