mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 15:04:01 +00:00
Cached native biome handles
- Biome wrappers read canonical keys only on cache insertion - Native handle identity prevents stale biome reuse after registry replacement - The biome wrapper cache retains at most 4096 entries - Regression tests cover key reuse, namespaces, replacement handles, and eviction
This commit is contained in:
@@ -19,43 +19,33 @@
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.block.Biome;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Interned Bukkit adapter for a neutral biome handle.
|
||||
*/
|
||||
public final class BukkitBiome implements PlatformBiome {
|
||||
private static final ConcurrentHashMap<String, BukkitBiome> CACHE = new ConcurrentHashMap<>();
|
||||
private static final Cache<Biome, BukkitBiome> CACHE = Caffeine.newBuilder()
|
||||
.weakKeys()
|
||||
.maximumSize(4_096)
|
||||
.build();
|
||||
|
||||
private final Biome biome;
|
||||
private final String key;
|
||||
private final String namespace;
|
||||
|
||||
private BukkitBiome(Biome biome, String key) {
|
||||
private BukkitBiome(Biome biome) {
|
||||
this.biome = biome;
|
||||
this.key = key;
|
||||
int colon = key.indexOf(':');
|
||||
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
|
||||
NamespacedKey biomeKey = biome.getKey();
|
||||
this.key = biomeKey.toString();
|
||||
this.namespace = biomeKey.getNamespace();
|
||||
}
|
||||
|
||||
public static BukkitBiome of(Biome biome) {
|
||||
String key = biomeKey(biome);
|
||||
return CACHE.computeIfAbsent(key, (String k) -> new BukkitBiome(biome, k));
|
||||
}
|
||||
|
||||
private static String biomeKey(Biome biome) {
|
||||
for (String method : new String[]{"getKeyOrNull", "getKeyOrThrow", "getKey"}) {
|
||||
try {
|
||||
Object key = Biome.class.getMethod(method).invoke(biome);
|
||||
if (key != null) {
|
||||
return key.toString();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return biome.toString();
|
||||
return CACHE.get(biome, BukkitBiome::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class BukkitBiomeTest {
|
||||
private Class<?> biomeType;
|
||||
private Method wrap;
|
||||
|
||||
@Before
|
||||
public void loadAdapterWithoutServerRegistryInitialization() throws Exception {
|
||||
ClassLoader loader = new BiomeClassLoader();
|
||||
biomeType = loader.loadClass("org.bukkit.block.Biome");
|
||||
wrap = loader.loadClass(BukkitBiome.class.getName()).getMethod("of", biomeType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedHandlesKeepCanonicalIdentityWithoutReadingTheKeyAgain() throws Exception {
|
||||
AtomicInteger keyReads = new AtomicInteger();
|
||||
Object biome = biome("iris", "riverbank", keyReads);
|
||||
|
||||
PlatformBiome wrapped = wrap(biome);
|
||||
assertEquals("iris:riverbank", wrapped.key());
|
||||
assertEquals("iris", wrapped.namespace());
|
||||
assertSame(biome, wrapped.nativeHandle());
|
||||
assertSame(wrapped, wrap(biome));
|
||||
assertSame(wrapped, wrap(biome));
|
||||
assertEquals(1, keyReads.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementRegistryHandleCannotReturnThePreviousNativeBiome() throws Exception {
|
||||
Object previous = biome("iris", "registry_replacement", new AtomicInteger());
|
||||
Object replacement = biome("iris", "registry_replacement", new AtomicInteger());
|
||||
|
||||
PlatformBiome oldHandle = wrap(previous);
|
||||
PlatformBiome newHandle = wrap(replacement);
|
||||
|
||||
assertNotSame(oldHandle, newHandle);
|
||||
assertEquals(oldHandle.key(), newHandle.key());
|
||||
assertSame(previous, oldHandle.nativeHandle());
|
||||
assertSame(replacement, newHandle.nativeHandle());
|
||||
assertSame(newHandle, wrap(replacement));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customAndVanillaNamespacesRemainDistinct() throws Exception {
|
||||
PlatformBiome vanilla = wrap(biome("minecraft", "plains", new AtomicInteger()));
|
||||
PlatformBiome custom = wrap(biome("iris", "plains", new AtomicInteger()));
|
||||
|
||||
assertEquals("minecraft:plains", vanilla.key());
|
||||
assertEquals("minecraft", vanilla.namespace());
|
||||
assertEquals("iris:plains", custom.key());
|
||||
assertNotSame(vanilla, custom);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheEvictionPreservesBiomeKeysAndNativeHandles() throws Exception {
|
||||
Object retained = biome("iris", "retained", new AtomicInteger());
|
||||
PlatformBiome original = wrap(retained);
|
||||
for (int index = 0; index < 8_192; index++) {
|
||||
wrap(biome("iris", "eviction_" + index, new AtomicInteger()));
|
||||
}
|
||||
Field field = wrap.getDeclaringClass().getDeclaredField("CACHE");
|
||||
field.setAccessible(true);
|
||||
Cache<?, ?> cache = (Cache<?, ?>) field.get(null);
|
||||
cache.cleanUp();
|
||||
|
||||
assertTrue(cache.estimatedSize() <= 4_096);
|
||||
PlatformBiome restored = wrap(retained);
|
||||
assertEquals(original.key(), restored.key());
|
||||
assertEquals(original.namespace(), restored.namespace());
|
||||
assertSame(retained, restored.nativeHandle());
|
||||
}
|
||||
|
||||
private PlatformBiome wrap(Object biome) throws Exception {
|
||||
return (PlatformBiome) wrap.invoke(null, biome);
|
||||
}
|
||||
|
||||
private Object biome(String namespace, String name, AtomicInteger keyReads) {
|
||||
NamespacedKey key = new NamespacedKey(namespace, name);
|
||||
return Proxy.newProxyInstance(biomeType.getClassLoader(), new Class<?>[]{biomeType}, (proxy, method, arguments) -> switch (method.getName()) {
|
||||
case "getKey" -> {
|
||||
keyReads.incrementAndGet();
|
||||
yield key;
|
||||
}
|
||||
case "hashCode" -> System.identityHashCode(proxy);
|
||||
case "equals" -> proxy == arguments[0];
|
||||
case "toString" -> key.toString();
|
||||
default -> throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
}
|
||||
|
||||
private static final class BiomeClassLoader extends ClassLoader {
|
||||
private BiomeClassLoader() {
|
||||
super(BukkitBiomeTest.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (!name.equals("org.bukkit.block.Biome") && !name.equals(BukkitBiome.class.getName())) {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
Class<?> loaded = findLoadedClass(name);
|
||||
if (loaded == null) {
|
||||
try (InputStream source = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) {
|
||||
if (source == null) {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
byte[] bytecode = source.readAllBytes();
|
||||
if (name.equals("org.bukkit.block.Biome")) {
|
||||
ClassWriter writer = new ClassWriter(0);
|
||||
new ClassReader(bytecode).accept(new ClassVisitor(Opcodes.ASM9, writer) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String method, String descriptor, String signature, String[] exceptions) {
|
||||
return method.equals("<clinit>") ? null : super.visitMethod(access, method, descriptor, signature, exceptions);
|
||||
}
|
||||
}, 0);
|
||||
bytecode = writer.toByteArray();
|
||||
}
|
||||
loaded = defineClass(name, bytecode, 0, bytecode.length);
|
||||
} catch (IOException exception) {
|
||||
throw new ClassNotFoundException(name, exception);
|
||||
}
|
||||
}
|
||||
if (resolve) {
|
||||
resolveClass(loaded);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user