Thread-id table for engine frames

- frames live in a 256-slot table indexed by thread id, ThreadLocal only on slot collisions
- the single ThreadLocal probed long collision chains in the workers' thread-local maps
This commit is contained in:
Brian Neumann-Fopiano
2026-09-28 11:57:02 -04:00
parent f2236a8482
commit 34f00d86cf
2 changed files with 85 additions and 16 deletions
@@ -23,24 +23,38 @@ import art.arcane.iris.generation.runtime.IrisEngine.BiomeEnvironmentBinding;
import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeBinding;
import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeScope;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
/**
* 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.
* Every per-thread engine binding (runtime assembly, biome environment, generation runtime scope) in one frame.
* The engine getters run on generation hot paths, where a ThreadLocal lookup probes long collision chains in the
* workers' crowded thread-local maps. Frames therefore live in a table indexed by thread id and owned by their
* thread; a frame whose slot is taken by another thread falls back to a ThreadLocal.
*/
final class EngineThreadState {
private final ThreadLocal<Frame> frames = new ThreadLocal<>();
private static final int SLOTS = 256;
private final AtomicReferenceArray<Frame> slots = new AtomicReferenceArray<>(SLOTS);
private final ThreadLocal<Frame> overflow = new ThreadLocal<>();
private final AtomicInteger overflowFrames = new AtomicInteger();
Frame current() {
return frames.get();
Thread thread = Thread.currentThread();
Frame frame = slots.getPlain(slot(thread));
if (frame != null && frame.owner == thread) {
return frame;
}
return overflowFrames.get() == 0 ? null : overflow.get();
}
GenerationRuntimeBinding binding() {
Frame frame = frames.get();
Frame frame = current();
return frame == null ? null : frame.binding;
}
RuntimeAssembly assembly() {
Frame frame = frames.get();
Frame frame = current();
return frame == null ? null : frame.assembly;
}
@@ -49,7 +63,7 @@ final class EngineThreadState {
frame().assembly = assembly;
return;
}
Frame frame = frames.get();
Frame frame = current();
if (frame != null) {
frame.assembly = null;
releaseIfEmpty(frame);
@@ -57,7 +71,7 @@ final class EngineThreadState {
}
BiomeEnvironmentBinding environment() {
Frame frame = frames.get();
Frame frame = current();
return frame == null ? null : frame.environment;
}
@@ -66,7 +80,7 @@ final class EngineThreadState {
frame().environment = environment;
return;
}
Frame frame = frames.get();
Frame frame = current();
if (frame != null) {
frame.environment = null;
releaseIfEmpty(frame);
@@ -89,7 +103,7 @@ final class EngineThreadState {
if (Thread.currentThread() != owner) {
throw new IllegalStateException("Iris generation runtime scope closed from a different thread.");
}
Frame frame = frames.get();
Frame frame = current();
if ((frame == null ? null : frame.binding) != installed) {
throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order.");
}
@@ -104,23 +118,45 @@ final class EngineThreadState {
}
private Frame frame() {
Frame frame = frames.get();
if (frame == null) {
frame = new Frame();
frames.set(frame);
Frame frame = current();
if (frame != null) {
return frame;
}
Thread thread = Thread.currentThread();
frame = new Frame(thread);
if (!slots.compareAndSet(slot(thread), null, frame)) {
frame.overflow = true;
overflow.set(frame);
overflowFrames.incrementAndGet();
}
return frame;
}
private void releaseIfEmpty(Frame frame) {
if (frame.assembly == null && frame.environment == null && frame.binding == null) {
frames.remove();
if (frame.assembly != null || frame.environment != null || frame.binding != null) {
return;
}
if (frame.overflow) {
overflow.remove();
overflowFrames.decrementAndGet();
return;
}
slots.compareAndSet(slot(frame.owner), frame, null);
}
private static int slot(Thread thread) {
return (int) thread.threadId() & (SLOTS - 1);
}
static final class Frame {
private final Thread owner;
private boolean overflow;
RuntimeAssembly assembly;
BiomeEnvironmentBinding environment;
GenerationRuntimeBinding binding;
private Frame(Thread owner) {
this.owner = owner;
}
}
}
@@ -49,6 +49,39 @@ public class EngineThreadStateTest {
assertNull(state.current());
}
@Test
public void threadsSharingATableSlotKeepSeparateFrames() throws InterruptedException {
EngineThreadState state = new EngineThreadState();
IrisEngine.GenerationRuntimeBinding mainBinding = new IrisEngine.GenerationRuntimeBinding(
mock(IrisEngine.class), mock(GenerationRuntime.class));
IrisEngine.GenerationRuntimeBinding otherBinding = new IrisEngine.GenerationRuntimeBinding(
mock(IrisEngine.class), mock(GenerationRuntime.class));
IrisEngine.GenerationRuntimeScope mainScope = state.open(mainBinding);
long slot = Thread.currentThread().threadId() & 255;
AtomicReference<Object> otherSeen = new AtomicReference<>();
AtomicReference<Object> otherAfterClose = new AtomicReference<>(otherBinding);
Thread colliding = null;
while (colliding == null) {
Thread candidate = new Thread(() -> {
IrisEngine.GenerationRuntimeScope scope = state.open(otherBinding);
otherSeen.set(state.binding());
scope.close();
otherAfterClose.set(state.current());
});
if ((candidate.threadId() & 255) == slot) {
colliding = candidate;
}
}
colliding.start();
colliding.join();
assertSame(otherBinding, otherSeen.get());
assertNull(otherAfterClose.get());
assertSame(mainBinding, state.binding());
mainScope.close();
assertNull(state.current());
}
@Test
public void framesAreThreadConfinedAndScopesRejectForeignClose() throws InterruptedException {
EngineThreadState state = new EngineThreadState();