One thread frame per engine

- runtime assembly, biome environment and runtime scope share one ThreadLocal frame
- engine getters do a single ThreadLocal lookup
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-28 11:17:51 -04:00
1 parent badca54e5d
commit d0008086ac
11 files changed
+296 -151

No files matched your search

@@ -81,7 +81,7 @@ final class EngineHotloader {
previousGeneration.transitionPlan()); previousGeneration.transitionPlan());
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture( PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
assembly.target, assembly.runtimeKernel, assembly.transitionPlan, engine.isStudio()); assembly.target, assembly.runtimeKernel, assembly.transitionPlan, engine.isStudio());
engine.runtimeAssembly.set(assembly); engine.threadState.setAssembly(assembly);
EngineRuntime next; EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan, false); assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan, false);
@@ -97,7 +97,7 @@ final class EngineHotloader {
biomeMaxes); biomeMaxes);
next = previous.withGeneration(nextGeneration); next = previous.withGeneration(nextGeneration);
} finally { } finally {
engine.runtimeAssembly.remove(); engine.threadState.setAssembly(null);
} }
Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close); Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close);
if (retirementFailure != null) { if (retirementFailure != null) {
@@ -93,7 +93,7 @@ final class EngineRuntimeBuilder {
mantleStorageDirectory, mantleStorageDirectory,
selectRuntimeKernel(kernelVersion), selectRuntimeKernel(kernelVersion),
transitionPlan); transitionPlan);
engine.runtimeAssembly.set(assembly); engine.threadState.setAssembly(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Engine " + assembly.cacheId); IrisLogging.debug("Setup Engine " + assembly.cacheId);
long started = M.ms(); long started = M.ms();
@@ -148,7 +148,7 @@ final class EngineRuntimeBuilder {
} }
throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e); throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e);
} finally { } finally {
engine.runtimeAssembly.remove(); engine.threadState.setAssembly(null);
} }
} }
@@ -174,7 +174,7 @@ final class EngineRuntimeBuilder {
transitionPlan); transitionPlan);
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture( PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
runtimeTarget, assembly.runtimeKernel, transitionPlan, engine.isStudio()); 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)) { try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId); IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId);
assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory); 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); throw new IllegalStateException("Failed to build a detached Iris generation runtime.", e);
} finally { } finally {
engine.runtimeAssembly.remove(); engine.threadState.setAssembly(null);
} }
} }
@@ -264,20 +264,16 @@ final class EngineShutdownSequence {
return failure; return failure;
} }
incompleteAssemblies.add(assembly); incompleteAssemblies.add(assembly);
RuntimeAssembly previous = engine == null ? null : engine.runtimeAssembly.get(); RuntimeAssembly previous = engine == null ? null : engine.threadState.assembly();
if (engine != null) { if (engine != null) {
engine.runtimeAssembly.set(assembly); engine.threadState.setAssembly(assembly);
} }
Throwable assemblyFailure; Throwable assemblyFailure;
try { try {
assemblyFailure = closeAssemblyResources(assembly); assemblyFailure = closeAssemblyResources(assembly);
} finally { } finally {
if (engine != null) { if (engine != null) {
if (previous == null) { engine.threadState.setAssembly(previous);
engine.runtimeAssembly.remove();
} else {
engine.runtimeAssembly.set(previous);
}
} }
} }
if (assemblyFailure == null) { if (assemblyFailure == null) {
@@ -401,7 +397,7 @@ final class EngineShutdownSequence {
engine, engine,
generationRuntime); generationRuntime);
Throwable runtimeFailure; Throwable runtimeFailure;
try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) { try (IrisEngine.GenerationRuntimeScope ignored = engine.threadState.open(binding)) {
runtimeFailure = closeGenerationRuntime(generationRuntime, null); runtimeFailure = closeGenerationRuntime(generationRuntime, null);
} catch (Throwable scopeFailure) { } catch (Throwable scopeFailure) {
runtimeFailure = scopeFailure; runtimeFailure = scopeFailure;
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Frame> 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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<GenerationRuntimeBinding> 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);
}
}
@@ -117,13 +117,7 @@ public class IrisEngine implements Engine {
final Object generationHistoryRuntimeRouterLock = new Object(); final Object generationHistoryRuntimeRouterLock = new Object();
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
final ThreadLocal<RuntimeAssembly> runtimeAssembly = new ThreadLocal<>(); final EngineThreadState threadState = new EngineThreadState();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final ThreadLocal<BiomeEnvironmentBinding> biomeEnvironmentScopes = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final GenerationRuntimeScopeState generationRuntimeScopes = new GenerationRuntimeScopeState();
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
final Set<GenerationRuntime> detachedGenerationRuntimes = Collections.synchronizedSet( final Set<GenerationRuntime> 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); throw new IllegalStateException("World entry hydrology preparation could not acquire its generation session.", failure);
} }
try (lease; try (lease;
GenerationRuntimeScope runtimeScope = generationRuntimeScopes.open(binding); GenerationRuntimeScope runtimeScope = threadState.open(binding);
IrisContext.Scope context = IrisContext.open(this, lease.sessionId(), null)) { IrisContext.Scope context = IrisContext.open(this, lease.sessionId(), null)) {
getComplex().getHydrologyRuntime().prepareChunkColumns(blockX, blockZ); getComplex().getHydrologyRuntime().prepareChunkColumns(blockX, blockZ);
} catch (Exception failure) { } catch (Exception failure) {
@@ -573,8 +567,8 @@ public class IrisEngine implements Engine {
@Override @Override
public BiomeEnvironment.Scope openBiomeEnvironmentScope(BiomeEnvironment environment) { public BiomeEnvironment.Scope openBiomeEnvironmentScope(BiomeEnvironment environment) {
BiomeEnvironmentBinding binding = new BiomeEnvironmentBinding( BiomeEnvironmentBinding binding = new BiomeEnvironmentBinding(
Objects.requireNonNull(environment, "environment"), biomeEnvironmentScopes.get()); Objects.requireNonNull(environment, "environment"), threadState.environment());
biomeEnvironmentScopes.set(binding); threadState.setEnvironment(binding);
return binding; return binding;
} }
@@ -980,7 +974,7 @@ public class IrisEngine implements Engine {
} }
public boolean hasGenerationRuntimeScope() { public boolean hasGenerationRuntimeScope() {
return generationRuntimeScopes.current() != null; return threadState.binding() != null;
} }
private boolean usesScopedNaturalTerrain() { private boolean usesScopedNaturalTerrain() {
@@ -1059,7 +1053,7 @@ public class IrisEngine implements Engine {
} }
public GenerationRuntimeBinding captureGenerationRuntimeBinding() { public GenerationRuntimeBinding captureGenerationRuntimeBinding() {
GenerationRuntimeBinding scoped = generationRuntimeScopes.current(); GenerationRuntimeBinding scoped = threadState.binding();
return scoped == null ? getActiveGenerationRuntimeBinding() : scoped; return scoped == null ? getActiveGenerationRuntimeBinding() : scoped;
} }
@@ -1075,7 +1069,7 @@ public class IrisEngine implements Engine {
if (!isGenerationRuntimeBindingLive(required)) { if (!isGenerationRuntimeBindingLive(required)) {
throw new IllegalStateException("Iris generation runtime binding is closed or no longer owned."); 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 @Override
public SeedManager getSeedManager() { public SeedManager getSeedManager() {
RuntimeAssembly assembly = runtimeAssembly.get(); EngineThreadState.Frame frame = threadState.current();
RuntimeAssembly assembly = frame == null ? null : frame.assembly;
if (assembly != null) { if (assembly != null) {
return assembly.seedManager; return assembly.seedManager;
} }
GenerationRuntime current = selectedGenerationRuntime(); GenerationRuntime current = selectedGenerationRuntime(frame);
return current == null ? seedManager : current.seedManager(); return current == null ? seedManager : current.seedManager();
} }
@Override @Override
public IrisComplex getComplex() { 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) { if (assembly != null && assembly.complex != null) {
return assembly.complex; return assembly.complex;
} }
GenerationRuntime current = selectedGenerationRuntime(); GenerationRuntime current = selectedGenerationRuntime(frame);
return current == null ? null : current.complex(); return current == null ? null : current.complex();
} }
@Override @Override
public EngineTarget getTarget() { public EngineTarget getTarget() {
RuntimeAssembly assembly = runtimeAssembly.get(); EngineThreadState.Frame frame = threadState.current();
RuntimeAssembly assembly = frame == null ? null : frame.assembly;
if (assembly != null) { if (assembly != null) {
return assembly.target; return assembly.target;
} }
GenerationRuntime current = selectedGenerationRuntime(); GenerationRuntime current = selectedGenerationRuntime(frame);
return current == null ? publishedTarget : current.target(); return current == null ? publishedTarget : current.target();
} }
@Override @Override
public IrisData getData() { public IrisData getData() {
BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get(); EngineThreadState.Frame frame = threadState.current();
if (environment != null && !hasGenerationRuntimeScope()) { if (frame != null) {
return environment.environment.data(); 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(); GenerationRuntime current = selectedGenerationRuntime(frame);
if (assembly != null) {
return assembly.target.getData();
}
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? publishedTarget.getData() : current.data(); return current == null ? publishedTarget.getData() : current.data();
} }
@Override @Override
public IrisDimension getDimension() { public IrisDimension getDimension() {
BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get(); EngineThreadState.Frame frame = threadState.current();
if (environment != null && !hasGenerationRuntimeScope()) { if (frame != null) {
return environment.environment.dimension(); 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(); GenerationRuntime current = selectedGenerationRuntime(frame);
if (assembly != null) {
return assembly.target.getDimension();
}
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? publishedTarget.getDimension() : current.dimension(); return current == null ? publishedTarget.getDimension() : current.dimension();
} }
@Override @Override
public EngineMantle getMantle() { 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) { if (assembly != null && assembly.mantle != null) {
return assembly.mantle; return assembly.mantle;
} }
GenerationRuntime current = selectedGenerationRuntime(); GenerationRuntime current = selectedGenerationRuntime(frame);
return current == null ? null : current.mantle(); return current == null ? null : current.mantle();
} }
@Override @Override
public EngineMode getMode() { public EngineMode getMode() {
RuntimeAssembly assembly = runtimeAssembly.get(); RuntimeAssembly assembly = threadState.assembly();
if (assembly != null && assembly.mode != null) { if (assembly != null && assembly.mode != null) {
return assembly.mode; return assembly.mode;
} }
@@ -1344,7 +1348,7 @@ public class IrisEngine implements Engine {
@Override @Override
public EngineEffects getEffects() { public EngineEffects getEffects() {
RuntimeAssembly assembly = runtimeAssembly.get(); RuntimeAssembly assembly = threadState.assembly();
if (assembly != null && assembly.effects != null) { if (assembly != null && assembly.effects != null) {
return assembly.effects; return assembly.effects;
} }
@@ -1354,7 +1358,7 @@ public class IrisEngine implements Engine {
@Override @Override
public EngineWorldManager getWorldManager() { public EngineWorldManager getWorldManager() {
RuntimeAssembly assembly = runtimeAssembly.get(); RuntimeAssembly assembly = threadState.assembly();
if (assembly != null && assembly.worldManager != null) { if (assembly != null && assembly.worldManager != null) {
return assembly.worldManager; return assembly.worldManager;
} }
@@ -1364,27 +1368,29 @@ public class IrisEngine implements Engine {
@Override @Override
public UpperDimensionContext getUpperContext() { public UpperDimensionContext getUpperContext() {
RuntimeAssembly assembly = runtimeAssembly.get(); EngineThreadState.Frame frame = threadState.current();
RuntimeAssembly assembly = frame == null ? null : frame.assembly;
if (assembly != null) { if (assembly != null) {
return assembly.upperContext; return assembly.upperContext;
} }
GenerationRuntime current = selectedGenerationRuntime(); GenerationRuntime current = selectedGenerationRuntime(frame);
return current == null ? null : current.upperContext(); return current == null ? null : current.upperContext();
} }
@Override @Override
public DimensionStackContext getDimensionStackContext() { public DimensionStackContext getDimensionStackContext() {
RuntimeAssembly assembly = runtimeAssembly.get(); EngineThreadState.Frame frame = threadState.current();
RuntimeAssembly assembly = frame == null ? null : frame.assembly;
if (assembly != null) { if (assembly != null) {
return assembly.dimensionStackContext; return assembly.dimensionStackContext;
} }
GenerationRuntime current = selectedGenerationRuntime(); GenerationRuntime current = selectedGenerationRuntime(frame);
return current == null ? null : current.dimensionStackContext(); return current == null ? null : current.dimensionStackContext();
} }
@Override @Override
public CompletableFuture<Long> getHash32() { public CompletableFuture<Long> getHash32() {
RuntimeAssembly assembly = runtimeAssembly.get(); RuntimeAssembly assembly = threadState.assembly();
if (assembly != null && assembly.hash32 != null) { if (assembly != null && assembly.hash32 != null) {
return assembly.hash32; return assembly.hash32;
} }
@@ -1579,16 +1585,21 @@ public class IrisEngine implements Engine {
@Override @Override
public int getCacheID() { public int getCacheID() {
RuntimeAssembly assembly = runtimeAssembly.get(); EngineThreadState.Frame frame = threadState.current();
RuntimeAssembly assembly = frame == null ? null : frame.assembly;
if (assembly != null) { if (assembly != null) {
return assembly.cacheId; return assembly.cacheId;
} }
GenerationRuntime current = selectedGenerationRuntime(); GenerationRuntime current = selectedGenerationRuntime(frame);
return current == null ? -1 : current.cacheId(); return current == null ? -1 : current.cacheId();
} }
private GenerationRuntime selectedGenerationRuntime() { 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)) { if (scoped != null && isGenerationRuntimeBindingLive(scoped)) {
return scoped.runtime; return scoped.runtime;
} }
@@ -1597,7 +1608,7 @@ public class IrisEngine implements Engine {
} }
public GenerationKernelRegistry.Version getGenerationKernelVersion() { public GenerationKernelRegistry.Version getGenerationKernelVersion() {
RuntimeAssembly assembly = runtimeAssembly.get(); RuntimeAssembly assembly = threadState.assembly();
if (assembly != null) { if (assembly != null) {
return assembly.kernelVersion; return assembly.kernelVersion;
} }
@@ -1605,7 +1616,7 @@ public class IrisEngine implements Engine {
} }
public GenerationKernelRegistry.RuntimeKernel getGenerationRuntimeKernel() { public GenerationKernelRegistry.RuntimeKernel getGenerationRuntimeKernel() {
RuntimeAssembly assembly = runtimeAssembly.get(); RuntimeAssembly assembly = threadState.assembly();
if (assembly != null) { if (assembly != null) {
return assembly.runtimeKernel; return assembly.runtimeKernel;
} }
@@ -1613,7 +1624,7 @@ public class IrisEngine implements Engine {
} }
public TransitionGenerationPlan getTransitionGenerationPlan() { public TransitionGenerationPlan getTransitionGenerationPlan() {
RuntimeAssembly assembly = runtimeAssembly.get(); RuntimeAssembly assembly = threadState.assembly();
if (assembly != null) { if (assembly != null) {
return assembly.transitionPlan; return assembly.transitionPlan;
} }
@@ -1737,14 +1748,14 @@ public class IrisEngine implements Engine {
} }
public static final class GenerationRuntimeScope implements AutoCloseable { public static final class GenerationRuntimeScope implements AutoCloseable {
private final GenerationRuntimeScopeState state; private final EngineThreadState state;
private final Thread owner; private final Thread owner;
private final GenerationRuntimeBinding previous; private final GenerationRuntimeBinding previous;
private final GenerationRuntimeBinding installed; private final GenerationRuntimeBinding installed;
private boolean closed; private boolean closed;
GenerationRuntimeScope( GenerationRuntimeScope(
GenerationRuntimeScopeState state, EngineThreadState state,
Thread owner, Thread owner,
GenerationRuntimeBinding previous, GenerationRuntimeBinding previous,
GenerationRuntimeBinding installed 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 BiomeEnvironment environment;
private final BiomeEnvironmentBinding previous; private final BiomeEnvironmentBinding previous;
private final Thread owner = Thread.currentThread(); private final Thread owner = Thread.currentThread();
@@ -1785,14 +1796,10 @@ public class IrisEngine implements Engine {
if (closed) { if (closed) {
return; return;
} }
if (biomeEnvironmentScopes.get() != this) { if (threadState.environment() != this) {
throw new IllegalStateException("Biome environment scopes must close in reverse order."); throw new IllegalStateException("Biome environment scopes must close in reverse order.");
} }
if (previous == null) { threadState.setEnvironment(previous);
biomeEnvironmentScopes.remove();
} else {
biomeEnvironmentScopes.set(previous);
}
closed = true; closed = true;
} }
} }
@@ -112,7 +112,7 @@ public class EngineShutdownDrainTest {
doAnswer(invocation -> { doAnswer(invocation -> {
Future<?> continuation = worker.submit(() -> { Future<?> continuation = worker.submit(() -> {
try (IrisEngine.GenerationRuntimeScope ignored = fixture.engine.openGenerationRuntimeScope(binding)) { 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); assertSame(fixture.runtime, fixture.engine.runtime);
assertTrue(fixture.engine.getClosing().get()); assertTrue(fixture.engine.getClosing().get());
assertThrows(IllegalStateException.class, () -> fixture.engine.requireRunning("start generation")); assertThrows(IllegalStateException.class, () -> fixture.engine.requireRunning("start generation"));
@@ -513,16 +513,16 @@ public class EngineShutdownDrainTest {
assembly.mantle = mock(EngineMantle.class); assembly.mantle = mock(EngineMantle.class);
assembly.ownsMantle = true; assembly.ownsMantle = true;
RuntimeAssembly previous = mock(RuntimeAssembly.class); RuntimeAssembly previous = mock(RuntimeAssembly.class);
fixture.engine.runtimeAssembly.set(previous); fixture.engine.threadState.setAssembly(previous);
IllegalStateException failure = new IllegalStateException("Planner still active"); IllegalStateException failure = new IllegalStateException("Planner still active");
doAnswer(invocation -> { doAnswer(invocation -> {
assertSame(assembly, fixture.engine.runtimeAssembly.get()); assertSame(assembly, fixture.engine.threadState.assembly());
throw failure; throw failure;
}).when(assembly.complex).close(); }).when(assembly.complex).close();
assertSame(failure, fixture.shutdown.closeAssembly(assembly, null)); assertSame(failure, fixture.shutdown.closeAssembly(assembly, null));
assertSame(previous, fixture.engine.runtimeAssembly.get()); assertSame(previous, fixture.engine.threadState.assembly());
fixture.engine.runtimeAssembly.remove(); fixture.engine.threadState.setAssembly(null);
assertTrue(fixture.shutdown.retainsData(detachedTarget.getData())); assertTrue(fixture.shutdown.retainsData(detachedTarget.getData()));
assertTrue(fixture.shutdown.closeDetachedTarget(detachedTarget, null) instanceof IllegalStateException); assertTrue(fixture.shutdown.closeDetachedTarget(detachedTarget, null) instanceof IllegalStateException);
verify(detachedTarget, never()).close(); verify(detachedTarget, never()).close();
@@ -531,13 +531,13 @@ public class EngineShutdownDrainTest {
verify(fixture.target, never()).close(); verify(fixture.target, never()).close();
verifyNoInteractions(assembly.mantle); verifyNoInteractions(assembly.mantle);
doAnswer(invocation -> { doAnswer(invocation -> {
assertSame(assembly, fixture.engine.runtimeAssembly.get()); assertSame(assembly, fixture.engine.threadState.assembly());
return null; return null;
}).when(assembly.complex).close(); }).when(assembly.complex).close();
fixture.shutdown.close(); fixture.shutdown.close();
} }
assertNull(fixture.engine.runtimeAssembly.get()); assertNull(fixture.engine.threadState.assembly());
assertFalse(assembly.ownsMantle); assertFalse(assembly.ownsMantle);
assertFalse(fixture.shutdown.retainsData(detachedTarget.getData())); assertFalse(fixture.shutdown.retainsData(detachedTarget.getData()));
verify(assembly.mantle).close(); verify(assembly.mantle).close();
@@ -851,10 +851,9 @@ public class EngineShutdownDrainTest {
setField(engine, "shutdownSequence", shutdown); setField(engine, "shutdownSequence", shutdown);
setField(engine, "engineDataStore", mock(EngineDataStore.class)); setField(engine, "engineDataStore", mock(EngineDataStore.class));
setField(engine, "lifecycleLock", new Object()); setField(engine, "lifecycleLock", new Object());
setField(engine, "runtimeAssembly", new ThreadLocal<RuntimeAssembly>()); setField(engine, "threadState", new EngineThreadState());
setField(engine, "detachedGenerationRuntimes", ConcurrentHashMap.newKeySet()); setField(engine, "detachedGenerationRuntimes", ConcurrentHashMap.newKeySet());
setField(engine, "retiringGenerationRuntimes", ConcurrentHashMap.newKeySet()); setField(engine, "retiringGenerationRuntimes", ConcurrentHashMap.newKeySet());
setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState());
AtomicBoolean closing = new AtomicBoolean(); AtomicBoolean closing = new AtomicBoolean();
setField(engine, "closing", closing); setField(engine, "closing", closing);
when(engine.getClosing()).thenReturn(closing); when(engine.getClosing()).thenReturn(closing);
@@ -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<Object> seen = new AtomicReference<>(binding);
AtomicReference<Throwable> 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());
}
}
@@ -103,7 +103,7 @@ public class IrisCarveWallPaletteScopeTest {
IrisContext.Scope context = IrisContext.open(fixture.engine, 71L, fixture.context); IrisContext.Scope context = IrisContext.open(fixture.engine, 71L, fixture.context);
MockedStatic<TerrainMatterView> terrain = mockStatic(TerrainMatterView.class)) { MockedStatic<TerrainMatterView> terrain = mockStatic(TerrainMatterView.class)) {
if (mode == Mode.ASSEMBLY) { 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())) terrain.when(() -> TerrainMatterView.iterate(eq(fixture.chunk), eq(MatterCavern.class), any()))
.thenAnswer(call -> { .thenAnswer(call -> {
@@ -137,7 +137,7 @@ public class IrisCarveWallPaletteScopeTest {
verify(fixture.chunk).release(); verify(fixture.chunk).release();
assertEquals(1, fixture.dataLookups.get()); assertEquals(1, fixture.dataLookups.get());
} finally { } finally {
fixture.engine.runtimeAssembly.remove(); fixture.engine.threadState.setAssembly(null);
} }
} }
assertSame(fixture.active.data(), fixture.engine.getData()); assertSame(fixture.active.data(), fixture.engine.getData());
@@ -199,9 +199,7 @@ public class IrisCarveWallPaletteScopeTest {
active = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle); active = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle);
detached = 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, "lifecycleLock", new Object());
field(engine, IrisEngine.class, "runtimeAssembly", new ThreadLocal<EngineRuntimeBuilder.RuntimeAssembly>()); field(engine, IrisEngine.class, "threadState", new EngineThreadState());
field(engine, IrisEngine.class, "biomeEnvironmentScopes", new ThreadLocal<>());
field(engine, IrisEngine.class, "generationRuntimeScopes", new GenerationRuntimeScopeState());
field(engine, IrisEngine.class, "detachedGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>())); field(engine, IrisEngine.class, "detachedGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>()));
field(engine, IrisEngine.class, "retiringGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>())); field(engine, IrisEngine.class, "retiringGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>()));
engine.detachedGenerationRuntimes.add(detached); engine.detachedGenerationRuntimes.add(detached);
@@ -255,7 +253,7 @@ public class IrisCarveWallPaletteScopeTest {
private IrisBiome scopedBiome(IrisBiome biome) { private IrisBiome scopedBiome(IrisBiome biome) {
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope( try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(
new IrisEngine.GenerationRuntimeBinding(engine, active))) { 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; return biome;
} }
@@ -62,7 +62,7 @@ public class IrisEngineEntryHydrologyTest {
doAnswer(invocation -> { doAnswer(invocation -> {
assertSame(engine, IrisContext.require().getEngine()); assertSame(engine, IrisContext.require().getEngine());
assertEquals(93L, IrisContext.require().getGenerationSessionId()); assertEquals(93L, IrisContext.require().getGenerationSessionId());
assertSame(binding, engine.generationRuntimeScopes.current()); assertSame(binding, engine.threadState.binding());
return null; return null;
}).when(hydrology).prepareChunkColumns(0, 0); }).when(hydrology).prepareChunkColumns(0, 0);
AtomicReference<Callable<Void>> queued = new AtomicReference<>(); AtomicReference<Callable<Void>> queued = new AtomicReference<>();
@@ -75,7 +75,7 @@ public class IrisEngineEntryHydrologyTest {
verify(hydrology).prepareChunkColumns(0, 0); verify(hydrology).prepareChunkColumns(0, 0);
verify(lease).close(); verify(lease).close();
assertNull(IrisContext.get()); assertNull(IrisContext.get());
assertNull(engine.generationRuntimeScopes.current()); assertNull(engine.threadState.binding());
tasks.drainBackgroundTasks("test").requireComplete("test"); tasks.drainBackgroundTasks("test").requireComplete("test");
} }
} }
@@ -325,9 +325,9 @@ public class IrisEngineEntryHydrologyTest {
Field closing = IrisEngine.class.getDeclaredField("closing"); Field closing = IrisEngine.class.getDeclaredField("closing");
closing.setAccessible(true); closing.setAccessible(true);
closing.set(engine, new AtomicBoolean()); closing.set(engine, new AtomicBoolean());
Field scopes = IrisEngine.class.getDeclaredField("generationRuntimeScopes"); Field scopes = IrisEngine.class.getDeclaredField("threadState");
scopes.setAccessible(true); scopes.setAccessible(true);
scopes.set(engine, new GenerationRuntimeScopeState()); scopes.set(engine, new EngineThreadState());
Field lifecycle = IrisEngine.class.getDeclaredField("lifecycleLock"); Field lifecycle = IrisEngine.class.getDeclaredField("lifecycleLock");
lifecycle.setAccessible(true); lifecycle.setAccessible(true);
lifecycle.set(engine, new Object()); lifecycle.set(engine, new Object());
@@ -923,9 +923,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = mock(IrisEngine.class, CALLS_REAL_METHODS); IrisEngine engine = mock(IrisEngine.class, CALLS_REAL_METHODS);
setField(engine, "lifecycleLock", new Object()); setField(engine, "lifecycleLock", new Object());
setField(engine, "generationHistoryRuntimeRouterLock", new Object()); setField(engine, "generationHistoryRuntimeRouterLock", new Object());
setField(engine, "runtimeAssembly", new ThreadLocal<EngineRuntimeBuilder.RuntimeAssembly>()); setField(engine, "threadState", new EngineThreadState());
setField(engine, "biomeEnvironmentScopes", new ThreadLocal<Object>());
setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState());
Set<GenerationRuntime> detached = Collections.synchronizedSet( Set<GenerationRuntime> detached = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<GenerationRuntime, Boolean>())); Collections.newSetFromMap(new IdentityHashMap<GenerationRuntime, Boolean>()));
setField(engine, "detachedGenerationRuntimes", detached); setField(engine, "detachedGenerationRuntimes", detached);