Files
openide/platform/platform-tests/testSrc/com/intellij/ide/IdeEventQueueTest.java
Nikita Kudrin cdf91bf213 [benchmarks] Renaming PerformanceTest* => Benchmark*
GitOrigin-RevId: 9963b84d51e1062acc262a8d3d3de1409a708e3b
2024-07-23 18:20:01 +00:00

223 lines
9.9 KiB
Java

// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.DefaultLogger;
import com.intellij.openapi.extensions.ExtensionNotApplicableException;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.EmptyRunnable;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.testFramework.LoggedErrorProcessor;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.tools.ide.metrics.benchmark.Benchmark;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.TestTimeOut;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.concurrency.EdtExecutorService;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.InvocationEvent;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class IdeEventQueueTest extends LightPlatformTestCase {
public void testManyEventsStress() {
int N = 100000;
Benchmark.newBenchmark("Event queue dispatch", () -> {
UIUtil.dispatchAllInvocationEvents();
AtomicInteger count = new AtomicInteger();
for (int i = 0; i < N; i++) {
SwingUtilities.invokeLater(count::incrementAndGet);
}
UIUtil.dispatchAllInvocationEvents();
assertEquals(N, count.get());
}).start();
}
public void testKeyboardEventsAreDetected() throws InterruptedException {
assertTrue(EventQueue.isDispatchThread());
IdeEventQueue ideEventQueue = IdeEventQueue.getInstance();
assertSame(ideEventQueue, Toolkit.getDefaultToolkit().getSystemEventQueue());
PlatformTestUtil.dispatchAllEventsInIdeEventQueue();
Set<AWTEvent> isDispatched = new HashSet<>();
ideEventQueue.addDispatcher(e -> {
isDispatched.add(e);
LOG.debug("dispatch: "+e);
return false;
}, getTestRootDisposable());
ideEventQueue.addPostprocessor(e -> {
LOG.debug("post dispatch: "+e);
return false;
}, getTestRootDisposable());
ideEventQueue.addPostEventListener(e -> {
LOG.debug("post event hook: "+e);
return false;
}, getTestRootDisposable());
int posted = ideEventQueue.keyboardEventPosted.get();
int dispatched = ideEventQueue.keyboardEventDispatched.get();
KeyEvent pressX = new KeyEvent(new JLabel("myKeyPress"), KeyEvent.KEY_PRESSED, 1, InputEvent.ALT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK, 11, 'x');
postCarefully(pressX);
assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get());
assertEquals(dispatched, ideEventQueue.keyboardEventDispatched.get());
dispatchAllInvocationEventsUntilOtherEvent();
// either it's dispatched by this method or the f*@$ing VCSRefresh activity stomped in, started modal progress and consumed all events via IdeEventQueue.pumpEventsForHierarchy
assertTrue(isDispatched.contains(pressX) || isConsumed(pressX));
assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get());
assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get());
// do not react to other events
AWTEvent ev2 = new ActionEvent(new JLabel(), ActionEvent.ACTION_PERFORMED, "myCommand");
postCarefully(ev2);
assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get());
assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get());
dispatchAllInvocationEventsUntilOtherEvent();
// either it's dispatched by this method or the f*@$ing VCSRefresh activity stomped in, started modal progress and dispatched all events via IdeEventQueue.pumpEventsForHierarchy by itself
assertTrue(isDispatched.contains(ev2));
assertEquals(posted+1, ideEventQueue.keyboardEventPosted.get());
assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get());
KeyEvent keyRelease = new KeyEvent(new JLabel("myKeyRelease"), KeyEvent.KEY_RELEASED, 1, InputEvent.ALT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK, 11, 'x');
postCarefully(keyRelease);
assertEquals(posted+2, ideEventQueue.keyboardEventPosted.get());
assertEquals(dispatched+1, ideEventQueue.keyboardEventDispatched.get());
dispatchAllInvocationEventsUntilOtherEvent();
// either it's dispatched by this method or the f*@$ing VCSRefresh activity stomped in, started modal progress and consumed all events via IdeEventQueue.pumpEventsForHierarchy
assertTrue(isDispatched.contains(keyRelease) || isConsumed(keyRelease));
assertEquals(posted+2, ideEventQueue.keyboardEventPosted.get());
assertEquals(dispatched+2, ideEventQueue.keyboardEventDispatched.get());
}
private static void postCarefully(AWTEvent event) {
LOG.debug("posting " + event);
IdeEventQueue ideEventQueue = IdeEventQueue.getInstance();
boolean posted = ideEventQueue.doPostEvent(event);
assertTrue("Was not posted: "+event, posted);
boolean mustBeConsumed = event.getID() == ActionEvent.ACTION_PERFORMED;
assertEquals(mustBeConsumed, ReflectionUtil.getField(AWTEvent.class, event, boolean.class, "consumed").booleanValue());
assertTrue(ReflectionUtil.getField(AWTEvent.class, event, boolean.class, "isPosted"));
}
private static boolean isConsumed(InputEvent event) {
return event.isConsumed();
}
// need this because everybody can post some crazy stuff to IdeEventQueue, so we have to filter InvocationEvents out
private static void dispatchAllInvocationEventsUntilOtherEvent() throws InterruptedException {
while (true) {
AWTEvent event = PlatformTestUtil.dispatchNextEventIfAny();
LOG.debug("event dispatched in dispatchAll() "+event+"; -"+(event instanceof InvocationEvent ? "continuing" : "returning"));
if (!(event instanceof InvocationEvent)) break;
}
}
private static class MyException extends RuntimeException {
}
private static void throwMyException() {
throw new MyException();
}
private static void checkMyExceptionThrownImmediately() {
TestTimeOut t = TestTimeOut.setTimeout(10, TimeUnit.SECONDS);
while (true) {
try {
UIUtil.dispatchAllInvocationEvents();
}
catch (Throwable e) {
assertTrue(e.toString(), ExceptionUtil.causedBy(e, MyException.class));
break;
}
assertFalse(t.timedOut());
}
}
public void testExceptionInInvokeLateredRunnableMustThrowImmediatelyInTests() {
SwingUtilities.invokeLater(() -> throwMyException());
checkMyExceptionThrownImmediately();
}
public void testAppInvokeLateredRunnableMustThrowImmediatelyInTests() {
SwingUtilities.invokeLater(()->ApplicationManager.getApplication().invokeLater(()->throwMyException()));
checkMyExceptionThrownImmediately();
}
public void testEdtExecutorRunnableMustThrowImmediatelyInTests() {
ApplicationManager.getApplication().invokeLater(() -> throwMyException(), ModalityState.nonModal());
checkMyExceptionThrownImmediately();
}
public void testEdtScheduledExecutorRunnableMustThrowImmediatelyInTests() {
EdtExecutorService.getScheduledExecutorInstance().schedule(()->throwMyException(), 1, TimeUnit.MILLISECONDS);
checkMyExceptionThrownImmediately();
}
public void testNoExceptionEvenCreatedByThanosExtensionNotApplicableExceptionMustKillEDT() {
assertTrue(SwingUtilities.isEventDispatchThread());
DefaultLogger.disableStderrDumping(getTestRootDisposable());
throwInIdeEventQueueDispatch(ExtensionNotApplicableException.create(), null); // ControlFlowException silently ignored
throwInIdeEventQueueDispatch(new ProcessCanceledException(), null); // ControlFlowException silently ignored
Error error = new Error();
throwInIdeEventQueueDispatch(error, error);
}
private void throwInIdeEventQueueDispatch(@NotNull Throwable toThrow, Throwable expectedToBeLogged) {
AtomicBoolean run = new AtomicBoolean();
InvocationEvent event = new InvocationEvent(this, () -> {
run.set(true);
ExceptionUtil.rethrow(toThrow);
});
Runnable runnable = () -> {
IdeEventQueue ideEventQueue = IdeEventQueue.getInstance();
ideEventQueue.executeInProductionModeEvenThoughWeAreInTests(() -> ideEventQueue.dispatchEvent(event));
};
Throwable error;
if (expectedToBeLogged != null) {
error = LoggedErrorProcessor.executeAndReturnLoggedError(runnable);
}
else {
runnable.run();
error = null;
}
assertTrue(run.get());
assertSame(expectedToBeLogged, error);
}
public void testPumpEventsForHierarchyMustExitOnIsFutureDoneCondition() {
assertTrue(SwingUtilities.isEventDispatchThread());
var ideEventQueue = IdeEventQueue.getInstance();
var future = new CompletableFuture<>();
var cancelEventTime = TestTimeOut.setTimeout(2, TimeUnit.SECONDS);
var component = new JLabel();
AppExecutorUtil.getAppScheduledExecutorService().schedule(() -> SwingUtilities.invokeLater(EmptyRunnable.getInstance()), 100, TimeUnit.MILLISECONDS);
var start = System.nanoTime();
ideEventQueue.pumpEventsForHierarchy(component, future, __ -> {
if (cancelEventTime.isTimedOut()) {
future.complete(null);
}
// post InvocationEvent to give getNextEvent work to do
SwingUtilities.invokeLater(EmptyRunnable.getInstance());
});
var elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
// check that first, we did exit the pumpEventsForHierarchy and second, at the right moment
assertTrue(String.valueOf(elapsedMs), cancelEventTime.isTimedOut());
}
}