diff --git a/eventbus-test-jar/src/main/java/net/minecraftforge/eventbus/testjar/events/PassthroughEvent.java b/eventbus-test-jar/src/main/java/net/minecraftforge/eventbus/testjar/events/PassthroughEvent.java new file mode 100644 index 0000000..83ba153 --- /dev/null +++ b/eventbus-test-jar/src/main/java/net/minecraftforge/eventbus/testjar/events/PassthroughEvent.java @@ -0,0 +1,24 @@ +package net.minecraftforge.eventbus.testjar.events; + +import net.minecraftforge.eventbus.api.bus.EventBus; +import net.minecraftforge.eventbus.api.event.InheritableEvent; + +/** + * This event tests the passthrough optimisation. + *

An event is considered passthrough when all the following conditions are met:

+ * + *

When these conditions are met, the {@code EventBus.create(Impl.class)} call returns the EventBus instance of its + * parent, instead of creating a new EventBus for the child. All calls to the child go directly to the parent, saving + * memory.

+ */ +public sealed interface PassthroughEvent extends InheritableEvent { + EventBus BUS = EventBus.create(PassthroughEvent.class); + + record Impl() implements PassthroughEvent { + public static final EventBus BUS = EventBus.create(Impl.class); + } +} diff --git a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java index 7dd418a..eab87af 100644 --- a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java +++ b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java @@ -12,6 +12,7 @@ import net.minecraftforge.eventbus.api.event.RecordEvent; import net.minecraftforge.eventbus.api.event.characteristic.Cancellable; import net.minecraftforge.eventbus.api.event.characteristic.MonitorAware; +import net.minecraftforge.eventbus.testjar.events.PassthroughEvent; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -91,6 +92,17 @@ interface Child extends InheritableTestEvent {} ); } + /** + * Tests that the references of the {@code BUS} fields of {@link PassthroughEvent} and its child refer to + * the same instance. + * @see PassthroughEvent + */ + @Test + public void testPassthroughEventCreation() { + Assertions.assertSame(PassthroughEvent.BUS, PassthroughEvent.Impl.BUS); + Assertions.assertEquals(PassthroughEvent.BUS.hashCode(), PassthroughEvent.Impl.BUS.hashCode()); + } + /** * Tests that only cancellable events can be created with a {@link CancellableEventBus}. */ diff --git a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java index 7493308..d22baf5 100644 --- a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java +++ b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java @@ -9,6 +9,7 @@ import net.minecraftforge.eventbus.api.event.MutableEvent; import net.minecraftforge.eventbus.api.event.characteristic.Cancellable; import net.minecraftforge.eventbus.internal.EventBusImpl; +import net.minecraftforge.eventbus.testjar.events.PassthroughEvent; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -231,4 +232,31 @@ final class SubEvent extends SuperEvent implements Cancellable { SuperEvent.BUS.removeListener(listener); } + + /** + * Tests that listener inheritance works when posting in a passthrough context. + * @see PassthroughEvent + */ + @Test + public void testListenerCallInheritanceWithPassthrough() { + var handled = new AtomicBoolean(); + var listener = PassthroughEvent.BUS.addListener(event -> handled.set(true)); + + Assertions.assertFalse(handled.get(), "PassthroughEvent should not be handled yet"); + + Assertions.assertDoesNotThrow(() -> PassthroughEvent.Impl.BUS.post(new PassthroughEvent.Impl())); + + Assertions.assertTrue(handled.get(), "PassthroughEvent should be handled"); + + PassthroughEvent.BUS.removeListener(listener); + handled.set(false); + + listener = PassthroughEvent.Impl.BUS.addListener(event -> handled.set(true)); + + Assertions.assertDoesNotThrow(() -> PassthroughEvent.Impl.BUS.post(new PassthroughEvent.Impl())); + + Assertions.assertTrue(handled.get(), "PassthroughEvent should be handled"); + + PassthroughEvent.BUS.removeListener(listener); + } } diff --git a/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java b/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java index faf5f93..cd7f91d 100644 --- a/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java +++ b/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java @@ -20,6 +20,7 @@ public sealed interface AbstractEventBusImpl extends EventBus permits CancellableEventBusImpl, EventBusImpl { //region Record component accessors + Class eventType(); ArrayList backingList(); ArrayList monitorBackingList(); List> children(); diff --git a/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java b/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java index f79bc9a..fe63478 100644 --- a/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java +++ b/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java @@ -94,8 +94,8 @@ private EventBus createEventBus(Class eventType) { int characteristics = AbstractEventBusImpl.computeEventCharacteristics(eventType); + boolean isRecord = (Constants.STRICT_BUS_CREATION_CHECKS || Constants.isInheritable(characteristics)) && eventType.isRecord(); if (Constants.STRICT_BUS_CREATION_CHECKS) { - boolean isRecord = eventType.isRecord(); if (!isRecord && RecordEvent.class.isAssignableFrom(eventType)) throw new IllegalArgumentException("Event type " + eventType + " implements RecordEvent but is not a record class"); @@ -113,13 +113,29 @@ private EventBus createEventBus(Class eventType) { } } - var backingList = new ArrayList(); + ArrayList backingList; List> parents = Collections.emptyList(); if (Constants.isInheritable(characteristics)) { parents = getParentEvents(eventType); + + // Direct pass-through of sealed inheritable events that only have one subclass to save memory (EventBus#97) + if ((isRecord || Modifier.isFinal(eventType.getModifiers())) // if this event is effectively final + && parents.size() == 1 // only has one parent EventBus + && parents.getFirst() instanceof AbstractEventBusImpl parent) { + var permittedSubclasses = parent.eventType().getPermittedSubclasses(); // that parent is sealed + if (permittedSubclasses != null && permittedSubclasses.length == 1 // only permits this event + && characteristics == parent.eventCharacteristics()) { // has the same characteristics + assert permittedSubclasses[0] == eventType; + return (EventBus) parents.getFirst(); // then we can reuse the parent directly + } + } + + backingList = new ArrayList<>(); for (var parent : parents) { backingList.addAll(((AbstractEventBusImpl) parent).backingList()); } + } else { + backingList = new ArrayList<>(); } @SuppressWarnings("rawtypes")