From 386693ee50f3096c207c03adef4dd6df7d126b76 Mon Sep 17 00:00:00 2001 From: Techcable Date: Thu, 3 Mar 2016 13:26:03 -0700 Subject: [PATCH] Use ASM for event execution Reflection (although highly optimized), has noticable overhead. It also creates noticable GC overhead by allocating arrays with arguments, as @Aikar has said. Based on WaterfallMC/Waterfall@1692934370735165f128fa8ece9a24fff5211094 Merge pull request #45 from Techcable/feature/asm-executors --- .../0023-Use-ASM-for-event-executors.patch | 351 ++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 Spigot-API-Patches/0023-Use-ASM-for-event-executors.patch diff --git a/Spigot-API-Patches/0023-Use-ASM-for-event-executors.patch b/Spigot-API-Patches/0023-Use-ASM-for-event-executors.patch new file mode 100644 index 0000000000..1535049cd6 --- /dev/null +++ b/Spigot-API-Patches/0023-Use-ASM-for-event-executors.patch @@ -0,0 +1,351 @@ +From 72795f24992711b90de1db3fa337de1ffbbe06d8 Mon Sep 17 00:00:00 2001 +From: Techcable +Date: Thu, 3 Mar 2016 13:20:33 -0700 +Subject: [PATCH] Use ASM for event executors. + +Uses method handles for private methods. + +diff --git a/pom.xml b/pom.xml +index 0b615b4..1ad108e 100644 +--- a/pom.xml ++++ b/pom.xml +@@ -112,6 +112,12 @@ + 1.3 + test + ++ ++ ++ org.ow2.asm ++ asm-all ++ 5.0.4 ++ + + + +diff --git a/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java b/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java +new file mode 100644 +index 0000000..9ff99e3 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java +@@ -0,0 +1,40 @@ ++package com.destroystokyo.paper.event.executor; ++ ++import java.lang.invoke.MethodHandle; ++import java.lang.invoke.MethodHandles; ++import java.lang.reflect.Method; ++ ++import org.bukkit.event.Event; ++import org.bukkit.event.EventException; ++import org.bukkit.event.Listener; ++import org.bukkit.plugin.EventExecutor; ++ ++public class MethodHandleEventExecutor implements EventExecutor { ++ private final Class eventClass; ++ private final MethodHandle handle; ++ ++ public MethodHandleEventExecutor(Class eventClass, MethodHandle handle) { ++ this.eventClass = eventClass; ++ this.handle = handle; ++ } ++ ++ public MethodHandleEventExecutor(Class eventClass, Method m) { ++ this.eventClass = eventClass; ++ try { ++ m.setAccessible(true); ++ this.handle = MethodHandles.lookup().unreflect(m); ++ } catch (IllegalAccessException e) { ++ throw new AssertionError("Unable to set accessible", e); ++ } ++ } ++ ++ @Override ++ public void execute(Listener listener, Event event) throws EventException { ++ if (!eventClass.isInstance(event)) return; ++ try { ++ handle.invoke(listener, event); ++ } catch (Throwable t) { ++ throw new EventException(t); ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java +new file mode 100644 +index 0000000..45c2330 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java +@@ -0,0 +1,44 @@ ++package com.destroystokyo.paper.event.executor.asm; ++ ++import java.lang.reflect.Method; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++import org.bukkit.plugin.EventExecutor; ++import org.objectweb.asm.ClassWriter; ++import org.objectweb.asm.Type; ++import org.objectweb.asm.commons.GeneratorAdapter; ++ ++import static org.objectweb.asm.Opcodes.*; ++ ++public class ASMEventExecutorGenerator { ++ public static byte[] generateEventExecutor(Method m, String name) { ++ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); ++ writer.visit(V1_8, ACC_PUBLIC, name.replace('.', '/'), null, Type.getInternalName(Object.class), new String[] {Type.getInternalName(EventExecutor.class)}); ++ // Generate constructor ++ GeneratorAdapter methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "", "()V", null, null), ACC_PUBLIC, "", "()V"); ++ methodGenerator.loadThis(); ++ methodGenerator.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "", "()V", false); // Invoke the super class (Object) constructor ++ methodGenerator.returnValue(); ++ methodGenerator.endMethod(); ++ // Generate the execute method ++ methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "execute", "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Event;)V", null, null), ACC_PUBLIC, "execute", "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Listener;)V");; ++ methodGenerator.loadArg(0); ++ methodGenerator.checkCast(Type.getType(m.getDeclaringClass())); ++ methodGenerator.loadArg(1); ++ methodGenerator.checkCast(Type.getType(m.getParameterTypes()[0])); ++ methodGenerator.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(m.getDeclaringClass()), m.getName(), Type.getMethodDescriptor(m), m.getDeclaringClass().isInterface()); ++ if (m.getReturnType() != void.class) { ++ methodGenerator.pop(); ++ } ++ methodGenerator.returnValue(); ++ methodGenerator.endMethod(); ++ writer.visitEnd(); ++ return writer.toByteArray(); ++ } ++ ++ public static AtomicInteger NEXT_ID = new AtomicInteger(1); ++ public static String generateName() { ++ int id = NEXT_ID.getAndIncrement(); ++ return "com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor" + id; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java +new file mode 100644 +index 0000000..6941d9f +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java +@@ -0,0 +1,32 @@ ++package com.destroystokyo.paper.event.executor.asm; ++ ++import com.destroystokyo.paper.utils.UnsafeUtils; ++ ++public interface ClassDefiner { ++ ++ /** ++ * Returns if the defined classes can bypass access checks ++ * ++ * @return if classes bypass access checks ++ */ ++ public default boolean isBypassAccessChecks() { ++ return false; ++ } ++ ++ /** ++ * Define a class ++ * ++ * @param parentLoader the parent classloader ++ * @param name the name of the class ++ * @param data the class data to load ++ * @return the defined class ++ * @throws ClassFormatError if the class data is invalid ++ * @throws NullPointerException if any of the arguments are null ++ */ ++ public Class defineClass(ClassLoader parentLoader, String name, byte[] data); ++ ++ public static ClassDefiner getInstance() { ++ return SafeClassDefiner.INSTANCE; ++ } ++ ++} +diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java +new file mode 100644 +index 0000000..776a9a0 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java +@@ -0,0 +1,62 @@ ++package com.destroystokyo.paper.event.executor.asm; ++ ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.ConcurrentMap; ++ ++import com.google.common.base.Preconditions; ++ ++import org.objectweb.asm.Type; ++ ++public class SafeClassDefiner implements ClassDefiner { ++ /* default */ static final SafeClassDefiner INSTANCE = new SafeClassDefiner(); ++ ++ private SafeClassDefiner() {} ++ ++ private final ConcurrentMap loaders = new ConcurrentHashMap<>(); ++ ++ @Override ++ public Class defineClass(ClassLoader parentLoader, String name, byte[] data) { ++ GeneratedClassLoader loader = loaders.computeIfAbsent(parentLoader, GeneratedClassLoader::new); ++ synchronized (loader.getClassLoadingLock(name)) { ++ Preconditions.checkState(!loader.hasClass(name), "%s already defined", name); ++ Class c = loader.define(name, data); ++ assert c.getName().equals(name); ++ return c; ++ } ++ } ++ ++ private static class GeneratedClassLoader extends ClassLoader { ++ static { ++ ClassLoader.registerAsParallelCapable(); ++ } ++ ++ protected GeneratedClassLoader(ClassLoader parent) { ++ super(parent); ++ } ++ ++ private Class define(String name, byte[] data) { ++ synchronized (getClassLoadingLock(name)) { ++ assert !hasClass(name); ++ Class c = defineClass(name, data, 0, data.length); ++ resolveClass(c); ++ return c; ++ } ++ } ++ ++ @Override ++ public Object getClassLoadingLock(String name) { ++ return super.getClassLoadingLock(name); ++ } ++ ++ public boolean hasClass(String name) { ++ synchronized (getClassLoadingLock(name)) { ++ try { ++ Class.forName(name); ++ return true; ++ } catch (ClassNotFoundException e) { ++ return false; ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/utils/UnsafeUtils.java b/src/main/java/com/destroystokyo/paper/utils/UnsafeUtils.java +new file mode 100644 +index 0000000..62acbf8 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/utils/UnsafeUtils.java +@@ -0,0 +1,33 @@ ++package com.destroystokyo.paper.utils; ++ ++import sun.misc.Unsafe; ++ ++import java.lang.reflect.Field; ++ ++public class UnsafeUtils { ++ private UnsafeUtils() {} ++ ++ private static final Unsafe UNSAFE; ++ static { ++ Unsafe unsafe; ++ try { ++ Class c = Class.forName("sun.misc.Unsafe"); ++ Field f = c.getDeclaredField("theUnsafe"); ++ f.setAccessible(true); ++ unsafe = (Unsafe) f.get(null); ++ } catch (ClassNotFoundException | NoSuchFieldException | SecurityException e) { ++ unsafe = null; ++ } catch (IllegalAccessException e) { ++ throw new AssertionError(e); ++ } ++ UNSAFE = unsafe; ++ } ++ ++ public static boolean isUnsafeSupported() { ++ return UNSAFE != null; ++ } ++ ++ public static Unsafe getUnsafe() { ++ return UNSAFE; ++ } ++} +diff --git a/src/main/java/org/bukkit/plugin/EventExecutor.java b/src/main/java/org/bukkit/plugin/EventExecutor.java +index 3b2c99e..7b92ce0 100644 +--- a/src/main/java/org/bukkit/plugin/EventExecutor.java ++++ b/src/main/java/org/bukkit/plugin/EventExecutor.java +@@ -4,9 +4,53 @@ import org.bukkit.event.Event; + import org.bukkit.event.EventException; + import org.bukkit.event.Listener; + ++// Paper start ++import java.lang.reflect.Method; ++import java.lang.reflect.Modifier; ++ ++import com.destroystokyo.paper.event.executor.MethodHandleEventExecutor; ++import com.destroystokyo.paper.event.executor.asm.ASMEventExecutorGenerator; ++import com.destroystokyo.paper.event.executor.asm.ClassDefiner; ++import com.google.common.base.Preconditions; ++// Paper end ++ + /** + * Interface which defines the class for event call backs to plugins + */ + public interface EventExecutor { + public void execute(Listener listener, Event event) throws EventException; ++ ++ // Paper start ++ public static EventExecutor create(Method m, Class eventClass) { ++ Preconditions.checkNotNull(m, "Null method"); ++ Preconditions.checkArgument(m.getParameterCount() != 0, "Incorrect number of arguments %s", m.getParameterCount()); ++ Preconditions.checkArgument(m.getParameterTypes()[0] == eventClass, "First parameter %s doesn't match event class %s", m.getParameterTypes()[0], eventClass); ++ Preconditions.checkArgument(!Modifier.isStatic(m.getModifiers()), "Static method %s", m.getName()); ++ ClassDefiner definer = ClassDefiner.getInstance(); ++ if (definer.isBypassAccessChecks() || Modifier.isPublic(m.getDeclaringClass().getModifiers()) && Modifier.isPublic(m.getModifiers())) { ++ String name = ASMEventExecutorGenerator.generateName(); ++ byte[] classData = ASMEventExecutorGenerator.generateEventExecutor(m, name); ++ Class c = definer.defineClass(m.getDeclaringClass().getClassLoader(), name, classData).asSubclass(EventExecutor.class); ++ try { ++ EventExecutor asmExecutor = c.newInstance(); ++ // Define a wrapper to conform to bukkit stupidity (passing in events that don't match and wrapper exception) ++ return new EventExecutor() { ++ @Override ++ public void execute(Listener listener, Event event) throws EventException { ++ if (!eventClass.isInstance(event)) return; ++ try { ++ asmExecutor.execute(listener, event); ++ } catch (Exception e) { ++ throw new EventException(e); ++ } ++ } ++ }; ++ } catch (InstantiationException | IllegalAccessException e) { ++ throw new AssertionError("Unable to initialize generated event executor", e); ++ } ++ } else { ++ return new MethodHandleEventExecutor(eventClass, m); ++ } ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +index 93a43dd..7229b25 100644 +--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +@@ -291,20 +291,7 @@ public final class JavaPluginLoader implements PluginLoader { + } + } + +- EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Spigot +- public void execute(Listener listener, Event event) throws EventException { +- try { +- if (!eventClass.isAssignableFrom(event.getClass())) { +- return; +- } +- method.invoke(listener, event); +- } catch (InvocationTargetException ex) { +- throw new EventException(ex.getCause()); +- } catch (Throwable t) { +- throw new EventException(t); +- } +- } +- }, plugin, method, eventClass); // Spigot ++ EventExecutor executor = new co.aikar.timings.TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass); // Spigot // Paper - Use factory method `EventExecutor.create()` + if (false) { // Spigot - RL handles useTimings check now + eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); + } else { +-- +2.7.2