Skip to content

Commit 184c091

Browse files
authored
Enhance alwaysCancelling listener awareness in invoker optimisation strategy (#82)
If the event is cancellable but all listeners are consumers, the invoker optimisation strategy would previously bail out to using the wrapped predicates if alwaysCancelling listeners were present. Now, alwaysCancelling listeners are properly accounted for in this case, combining the benefits of eliminating non-monitoring listeners after them with also unwrapping to branchless consumer calls in the built invoker.
1 parent 7878c1d commit 184c091

File tree

4 files changed

+169
-63
lines changed

4 files changed

+169
-63
lines changed

eventbus-test/src/test/java/net/minecraftforge/eventbus/test/IndividualEventListenerTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
import net.minecraftforge.eventbus.api.event.characteristic.Cancellable;
1111
import net.minecraftforge.eventbus.api.event.characteristic.SelfPosting;
1212
import net.minecraftforge.eventbus.api.listener.EventListener;
13+
import net.minecraftforge.eventbus.api.listener.Priority;
1314
import org.junit.jupiter.api.Assertions;
1415
import org.junit.jupiter.api.Test;
1516

17+
import java.util.List;
1618
import java.util.concurrent.atomic.AtomicBoolean;
1719
import java.util.concurrent.atomic.AtomicInteger;
1820
import java.util.concurrent.atomic.AtomicReference;
21+
import java.util.function.Consumer;
1922

2023
public class IndividualEventListenerTests {
2124
/**
@@ -105,6 +108,30 @@ record AlwaysCancellingTestEvent() implements Cancellable, RecordEvent {
105108
Assertions.assertFalse(wasCancelled, "The event should not have been cancelled without any listeners");
106109
}
107110

111+
@Test
112+
public void testAlwaysCancellingListenerOptimisation() throws Exception{
113+
record CancellableTestEvent() implements Cancellable, RecordEvent {
114+
static final CancellableEventBus<CancellableTestEvent> BUS = CancellableEventBus.create(CancellableTestEvent.class);
115+
}
116+
117+
var cancellingListener = CancellableTestEvent.BUS.addListener(Priority.HIGHEST, true, event -> {});
118+
var ordinaryListener = CancellableTestEvent.BUS.addListener(event -> {});
119+
120+
var listeners = List.of(cancellingListener, ordinaryListener);
121+
122+
// create a MH to net.minecraftforge.eventbus.internal.InvokerFactoryUtils.unwrapAlwaysCancellingConsumers
123+
var invokerFactoryUtilsClass = Class.forName("net.minecraftforge.eventbus.internal.InvokerFactoryUtils");
124+
var method = invokerFactoryUtilsClass.getDeclaredMethod("unwrapAlwaysCancellingConsumers", List.class);
125+
method.setAccessible(true);
126+
@SuppressWarnings("unchecked")
127+
var result = (List<Consumer<?>>) method.invoke(null, listeners);
128+
129+
Assertions.assertEquals(1, result.size(), "The ordinary listener should have been removed from the list");
130+
131+
CancellableTestEvent.BUS.removeListener(cancellingListener);
132+
CancellableTestEvent.BUS.removeListener(ordinaryListener);
133+
}
134+
108135
/**
109136
* Tests that exceptions thrown by listeners are propagated to the poster.
110137
*/

src/main/java/net/minecraftforge/eventbus/internal/Constants.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ private Constants() {}
1919

2020
static final Consumer<Event> NO_OP_CONSUMER = event -> {};
2121
static final Predicate<Event> NO_OP_PREDICATE = event -> false;
22+
static final Predicate<Event> ALWAYS_TRUE_PREDICATE = event -> true;
2223

2324
static final MethodHandle MH_NULL_CONSUMER = MethodHandles.constant(Consumer.class, null);
2425
static final MethodHandle MH_NO_OP_CONSUMER = MethodHandles.constant(Consumer.class, NO_OP_CONSUMER);
@@ -60,8 +61,8 @@ static <T> Consumer<T> getNoOpConsumer() {
6061
}
6162

6263
@SuppressWarnings("unchecked")
63-
static <T> Predicate<T> getNoOpPredicate() {
64-
return (Predicate<T>) NO_OP_PREDICATE;
64+
static <T> Predicate<T> getNoOpPredicate(boolean alwaysCancelling) {
65+
return (Predicate<T>) (alwaysCancelling ? ALWAYS_TRUE_PREDICATE : NO_OP_PREDICATE);
6566
}
6667

6768
static boolean isSelfDestructing(int characteristics) {

src/main/java/net/minecraftforge/eventbus/internal/InvokerFactory.java

Lines changed: 120 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,12 @@ private static <T extends Event & Cancellable> Predicate<T> createCancellableInv
194194
// ...so annoyingly, we need to duplicate the code of createInvoker() here, but with a different primitive return type (boolean instead of void)
195195
// Maybe JEP 402 can save us from this workaround in the future? https://openjdk.java.net/jeps/402
196196

197-
// Todo: [EB][Invoker] Support alwaysCancelling listeners in cancellation check-free Consumer invokers
198-
if (listeners.stream().map(EventListenerImpl.WrappedConsumerListener.class::cast).noneMatch(EventListenerImpl.WrappedConsumerListener::alwaysCancelling))
199-
return createCancellableInvokerFromUnwrappedNoChecks((List<Consumer<T>>) (List) InvokerFactoryUtils.unwrapConsumers(listeners));
197+
return createCancellableInvokerFromUnwrappedNoChecks(
198+
(List<Consumer<T>>) (List) InvokerFactoryUtils.unwrapAlwaysCancellingConsumers(listeners),
199+
listeners.stream()
200+
.map(EventListenerImpl.WrappedConsumerListener.class::cast)
201+
.anyMatch(EventListenerImpl.WrappedConsumerListener::alwaysCancelling)
202+
);
200203
}
201204

202205
return createCancellableInvokerFromUnwrapped((List<Predicate<T>>) (List) InvokerFactoryUtils.unwrapPredicates(listeners));
@@ -250,68 +253,127 @@ private static <T extends Event> Consumer<T> createInvokerFromUnwrapped(List<Con
250253
/**
251254
* Same as {@link #createInvokerFromUnwrapped(List)} but returns a {@link Predicate} instead of a {@link Consumer}.
252255
* <p>See the code comments inside {@link #createCancellableInvoker(List)} for an explainer as to why this exists.</p>
256+
* <p>Also see {@link EventListenerImpl.WrappedConsumerListener#wrap(boolean, Consumer)} for an explainer as to why capturing the return value is avoided.</p>
253257
*/
254-
private static <T extends Event & Cancellable> Predicate<T> createCancellableInvokerFromUnwrappedNoChecks(List<Consumer<T>> listeners) {
255-
return switch (listeners.size()) {
256-
case 0 -> Constants.getNoOpPredicate();
257-
case 1 -> {
258-
var first = listeners.getFirst();
259-
yield event -> {
260-
first.accept(event);
261-
return false;
262-
};
263-
}
264-
case 2 -> {
265-
var first = listeners.getFirst();
266-
var second = listeners.getLast();
267-
yield event -> {
268-
first.accept(event);
269-
second.accept(event);
270-
return false;
271-
};
272-
}
258+
private static <T extends Event & Cancellable> Predicate<T> createCancellableInvokerFromUnwrappedNoChecks(List<Consumer<T>> listeners, boolean alwaysCancelling) {
259+
if (alwaysCancelling) {
260+
return switch (listeners.size()) {
261+
case 0 -> Constants.getNoOpPredicate(true);
262+
case 1 -> {
263+
var first = listeners.getFirst();
264+
yield event -> {
265+
first.accept(event);
266+
return true;
267+
};
268+
}
269+
case 2 -> {
270+
var first = listeners.getFirst();
271+
var second = listeners.getLast();
272+
yield event -> {
273+
first.accept(event);
274+
second.accept(event);
275+
return true;
276+
};
277+
}
273278

274-
case 3 -> {
275-
var first = listeners.getFirst(); // 0
276-
var second = listeners.get(1);
277-
var third = listeners.getLast(); // 2
278-
yield event -> {
279-
first.accept(event);
280-
second.accept(event);
281-
third.accept(event);
282-
return false;
283-
};
284-
}
285-
case 4 -> {
286-
var first = listeners.getFirst(); // 0
287-
var second = listeners.get(1);
288-
var third = listeners.get(2);
289-
var fourth = listeners.getLast(); // 3
290-
yield event -> {
291-
first.accept(event);
292-
second.accept(event);
293-
third.accept(event);
294-
fourth.accept(event);
295-
return false;
296-
};
297-
}
279+
case 3 -> {
280+
var first = listeners.getFirst(); // 0
281+
var second = listeners.get(1);
282+
var third = listeners.getLast(); // 2
283+
yield event -> {
284+
first.accept(event);
285+
second.accept(event);
286+
third.accept(event);
287+
return true;
288+
};
289+
}
290+
case 4 -> {
291+
var first = listeners.getFirst(); // 0
292+
var second = listeners.get(1);
293+
var third = listeners.get(2);
294+
var fourth = listeners.getLast(); // 3
295+
yield event -> {
296+
first.accept(event);
297+
second.accept(event);
298+
third.accept(event);
299+
fourth.accept(event);
300+
return true;
301+
};
302+
}
298303

299-
default -> {
300-
@SuppressWarnings("unchecked")
301-
Consumer<T>[] listenersArray = listeners.toArray(new Consumer[0]);
302-
yield event -> {
303-
for (Consumer<T> listener : listenersArray) {
304-
listener.accept(event);
305-
}
306-
return false;
307-
};
308-
}
309-
};
304+
default -> {
305+
@SuppressWarnings("unchecked")
306+
Consumer<T>[] listenersArray = listeners.toArray(new Consumer[0]);
307+
yield event -> {
308+
for (Consumer<T> listener : listenersArray) {
309+
listener.accept(event);
310+
}
311+
return true;
312+
};
313+
}
314+
};
315+
} else {
316+
return switch (listeners.size()) {
317+
case 0 -> Constants.getNoOpPredicate(false);
318+
case 1 -> {
319+
var first = listeners.getFirst();
320+
yield event -> {
321+
first.accept(event);
322+
return false;
323+
};
324+
}
325+
case 2 -> {
326+
var first = listeners.getFirst();
327+
var second = listeners.getLast();
328+
yield event -> {
329+
first.accept(event);
330+
second.accept(event);
331+
return false;
332+
};
333+
}
334+
335+
case 3 -> {
336+
var first = listeners.getFirst(); // 0
337+
var second = listeners.get(1);
338+
var third = listeners.getLast(); // 2
339+
yield event -> {
340+
first.accept(event);
341+
second.accept(event);
342+
third.accept(event);
343+
return false;
344+
};
345+
}
346+
case 4 -> {
347+
var first = listeners.getFirst(); // 0
348+
var second = listeners.get(1);
349+
var third = listeners.get(2);
350+
var fourth = listeners.getLast(); // 3
351+
yield event -> {
352+
first.accept(event);
353+
second.accept(event);
354+
third.accept(event);
355+
fourth.accept(event);
356+
return false;
357+
};
358+
}
359+
360+
default -> {
361+
@SuppressWarnings("unchecked")
362+
Consumer<T>[] listenersArray = listeners.toArray(new Consumer[0]);
363+
yield event -> {
364+
for (Consumer<T> listener : listenersArray) {
365+
listener.accept(event);
366+
}
367+
return false;
368+
};
369+
}
370+
};
371+
}
310372
}
311373

312374
private static <T extends Event & Cancellable> Predicate<T> createCancellableInvokerFromUnwrapped(List<Predicate<T>> listeners) {
313375
return switch (listeners.size()) {
314-
case 0 -> Constants.getNoOpPredicate();
376+
case 0 -> Constants.getNoOpPredicate(false);
315377
case 1 -> listeners.getFirst(); // Direct call
316378
case 2 -> {
317379
var first = listeners.getFirst();

src/main/java/net/minecraftforge/eventbus/internal/InvokerFactoryUtils.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@
99

1010
import java.util.ArrayList;
1111
import java.util.List;
12-
import java.util.Set;
1312
import java.util.function.Consumer;
1413
import java.util.function.Predicate;
15-
import java.util.stream.Collectors;
1614

1715
final class InvokerFactoryUtils {
1816
private InvokerFactoryUtils() {}
@@ -30,11 +28,29 @@ static <T> List<Consumer<T>> unwrapConsumers(List<EventListener> listeners) {
3028
.toList();
3129
}
3230

31+
static <T> List<Consumer<T>> unwrapAlwaysCancellingConsumers(List<EventListener> listeners) {
32+
var unwrappedConsumers = new ArrayList<Consumer<T>>(listeners.size());
33+
for (var listener : listeners) {
34+
if (listener instanceof EventListenerImpl.HasConsumer<?> consumerListener) {
35+
unwrappedConsumers.add(uncheckedCast(consumerListener.consumer()));
36+
} else {
37+
throw new IllegalStateException("Unexpected listener type: " + listener.getClass());
38+
}
39+
40+
if (listener instanceof EventListenerImpl.WrappedConsumerListener wrappedConsumerListener
41+
&& wrappedConsumerListener.alwaysCancelling()) {
42+
unwrappedConsumers.trimToSize();
43+
break;
44+
}
45+
}
46+
return unwrappedConsumers;
47+
}
48+
3349
static <T> List<Predicate<T>> unwrapPredicates(List<EventListener> listeners) {
3450
var unwrappedPredicates = new ArrayList<Predicate<T>>(listeners.size());
3551
for (var listener : listeners) {
3652
if (listener instanceof EventListenerImpl.HasPredicate<?> predicateListener) {
37-
unwrappedPredicates.add(InvokerFactoryUtils.uncheckedCast(predicateListener.predicate()));
53+
unwrappedPredicates.add(uncheckedCast(predicateListener.predicate()));
3854
} else {
3955
throw new IllegalStateException("Unexpected listener type: " + listener.getClass());
4056
}

0 commit comments

Comments
 (0)