JBR-2031 [mac] jcef deadlocks with a11y on start

(cherry picked from commit 4f44b37f08)
(cherry picked from commit 8a4a781393)
This commit is contained in:
Anton Tarasov
2021-03-19 20:03:04 +03:00
committed by Vitaly Provodin
parent 3acb0de395
commit b2d79abea8
3 changed files with 165 additions and 159 deletions

View File

@@ -64,6 +64,7 @@ import com.apple.laf.ClientPropertyApplicator.Property;
import sun.awt.AWTAccessor;
import sun.awt.AWTAccessor.ComponentAccessor;
import sun.awt.AWTAccessor.WindowAccessor;
import sun.awt.InvokeOnToolkitHelper;
import sun.java2d.SurfaceData;
import sun.lwawt.LWKeyboardFocusManagerPeer;
import sun.lwawt.LWLightweightFramePeer;
@@ -345,7 +346,7 @@ public class CPlatformWindow extends CFRetainedResource implements PlatformWindo
long nativeWindowPtr = java.security.AccessController.doPrivileged(
(PrivilegedAction<Long>) () -> {
try {
return LWCToolkit.SelectorPerformer.perform(() -> {
return InvokeOnToolkitHelper.invokeAndBlock(() -> {
AtomicLong ref = new AtomicLong();
contentView.execute(viewPtr -> {
boolean hasOwnerPtr = false;

View File

@@ -85,23 +85,14 @@ import java.awt.peer.TaskbarPeer;
import java.awt.peer.TrayIconPeer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.net.MalformedURLException;
import javax.swing.UIManager;
import com.apple.laf.AquaMenuBarUI;
import sun.awt.AWTAccessor;
import sun.awt.AppContext;
import sun.awt.CGraphicsDevice;
import sun.awt.LightweightFrame;
import sun.awt.PlatformGraphicsInfo;
import sun.awt.SunToolkit;
import sun.awt.*;
import sun.awt.datatransfer.DataTransferer;
import sun.awt.dnd.SunDragSourceContextPeer;
import sun.awt.util.ThreadGroupUtils;
@@ -472,9 +463,9 @@ public final class LWCToolkit extends LWToolkit {
public Insets getScreenInsets(final GraphicsConfiguration gc) {
GraphicsDevice gd = gc.getDevice();
if (!(gd instanceof CGraphicsDevice)) {
return LWCToolkit.SelectorPerformer.perform(() -> super.getScreenInsets(gc));
return InvokeOnToolkitHelper.invokeAndBlock(() -> super.getScreenInsets(gc));
}
return LWCToolkit.SelectorPerformer.perform(() -> ((CGraphicsDevice)gd).getScreenInsets());
return InvokeOnToolkitHelper.invokeAndBlock(() -> ((CGraphicsDevice)gd).getScreenInsets());
}
@Override
@@ -677,150 +668,6 @@ public final class LWCToolkit extends LWToolkit {
}
}
/**
* Performs a wrapped native selector on the main thread, waiting on EDT, preventing the threads from a deadlock.
*/
public static class SelectorPerformer {
private ExecutorService executor;
// every perform() pushes a queue of invocations
private Stack<LinkedBlockingQueue<InvocationEvent>> invocations = new Stack<>();
// invocations should be dispatched on proper EDT (per AppContext)
private static final Map<Thread, SelectorPerformer> edt2performer = new ConcurrentHashMap<>();
private static final int WAIT_LIMIT_SECONDS = 5;
private SelectorPerformer() {}
/**
* Performs the selector wrapped in the callable. The selector should be executed via [JNFRunLoop performOnMainThreadWaiting:YES ...]
* on the native side so that the native doAWTRunLoop, which is run in [JNFRunLoop javaRunLoopMode], accepts it.
* The callable wrapper should not call any Java code which would normally be called on EDT.
* <p>
* If the main thread posts invocation events caused by the selector, those events are intercepted and dispatched on EDT out of order.
* <p>
* When called on non-EDT, the method performs the selector in place. The method is reentrant.
*
* @param selector the native selector wrapper
* @param <T> the selector return type
* @return the selector result
*/
public static <T> T perform(Callable<T> selector) {
if (selector == null) return null;
if (EventQueue.isDispatchThread()) {
SelectorPerformer performer = getInstance(Thread.currentThread());
if (performer != null) {
return performer.performImpl(selector);
}
}
// fallback to default
try {
return selector.call();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private <T> T performImpl(Callable<T> selector) {
assert EventQueue.isDispatchThread();
if (executor == null) {
// init on EDT
AccessController.doPrivileged((PrivilegedAction<?>)() ->
executor = new ThreadPoolExecutor(1, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadFactory() {
private ThreadFactory factory = Executors.privilegedThreadFactory();
@Override
public Thread newThread(Runnable r) {
Thread t = factory.newThread(r);
t.setDaemon(true);
t.setName("AWT-SelectorPerformer " + t.getName());
return t;
}
})
);
}
LinkedBlockingQueue<InvocationEvent> currentQueue;
synchronized (invocations) {
invocations.push(currentQueue = new LinkedBlockingQueue<>());
}
FutureTask<T> task = new FutureTask(selector) {
@Override
protected void done() {
synchronized (invocations) {
// Done with the current queue, wake it up.
invocations.pop().add(new InvocationEvent(executor, () -> {}));
}
}
};
executor.execute(task);
try {
while (!task.isDone() || !currentQueue.isEmpty()) {
InvocationEvent event = currentQueue.poll(WAIT_LIMIT_SECONDS, TimeUnit.SECONDS);
if (event == null) {
new RuntimeException("Waiting for the invocation event timed out").printStackTrace();
break;
}
event.dispatch();
}
return task.isDone() ? task.get() : null;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
/**
* Checks if there's an active SelectorPerformer corresponding to the invocation's AppContext,
* adds the invocation to the SelectorPerformer's queue and returns true.
* Otherwise does nothing and returns false.
*/
public static boolean offer(InvocationEvent invocation) {
Object source = invocation.getSource();
SelectorPerformer performer = (source instanceof Component) ?
getInstance((Component)source) :
getInstance(Toolkit.getDefaultToolkit().getSystemEventQueue());
if (performer == null) return false;
synchronized (performer.invocations) {
if (!performer.invocations.isEmpty()) {
performer.invocations.peek().add(invocation);
return true;
}
}
return false;
}
private static SelectorPerformer getInstance(Component comp) {
if (comp == null) return null;
AppContext appContext = SunToolkit.targetToAppContext(comp);
if (appContext == null) return null;
return getInstance((EventQueue)appContext.get(AppContext.EVENT_QUEUE_KEY));
}
private static SelectorPerformer getInstance(EventQueue eq) {
if (eq == null) return null;
return getInstance(AWTAccessor.getEventQueueAccessor().getDispatchThread(eq));
}
private static SelectorPerformer getInstance(Thread edt) {
if (edt == null) return null;
return edt2performer.computeIfAbsent(edt, key -> new SelectorPerformer());
}
}
/**
* Kicks an event over to the appropriate event queue and waits for it to
* finish To avoid deadlocking, we manually run the NSRunLoop while waiting
@@ -844,7 +691,7 @@ public final class LWCToolkit extends LWToolkit {
},
true);
if (!SelectorPerformer.offer(invocationEvent)) {
if (!InvokeOnToolkitHelper.offer(invocationEvent)) {
if (component != null) {
AppContext appContext = SunToolkit.targetToAppContext(component);
SunToolkit.postEvent(appContext, invocationEvent);
@@ -853,7 +700,7 @@ public final class LWCToolkit extends LWToolkit {
SunToolkit.flushPendingEvents(appContext);
} else {
// This should be the equivalent to EventQueue.invokeAndWait
((LWCToolkit) Toolkit.getDefaultToolkit()).getSystemEventQueueForInvokeAndWait().postEvent(invocationEvent);
((LWCToolkit) Toolkit.getDefaultToolkit()).getSystemEventQueueImpl().postEvent(invocationEvent);
}
}
doAWTRunLoop(mediator, false);

View File

@@ -0,0 +1,158 @@
package sun.awt;
import java.awt.*;
import java.awt.event.InvocationEvent;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.*;
/**
* Used to perform a blocking invocation on Toolkit thread from EDT, preventing a deadlock.
* The deadlock can happen when EDT and Toolkit perform blocking invocations at the same time.
* The following is performed to resolve it:
* 1) The invoker spins a nested event loop on EDT while waiting for the invocation to complete.
* 2) A separate pool thread is used to perform the invocation.
*/
public class InvokeOnToolkitHelper {
private ExecutorService executor;
// every invokeAndWait() pushes a queue of invocations
private final Stack<LinkedBlockingQueue<InvocationEvent>> invocations = new Stack<>();
// invocations should be dispatched on proper EDT (per AppContext)
private static final Map<Thread, InvokeOnToolkitHelper> edt2invokerMap = new ConcurrentHashMap<>();
private static final int WAIT_LIMIT_SECONDS = 5;
private InvokeOnToolkitHelper() {}
/**
* Invokes the callable on Toolkit thread and blocks until the completion. The method is re-entrant.
*
* On macOS it is assumed the callable wraps a native selector. The selector should be executed via [JNFRunLoop performOnMainThreadWaiting:YES ...]
* so that the doAWTRunLoop on AppKit (which is run in [JNFRunLoop javaRunLoopMode]) accepts it. The callable wrapper should not call any Java code
* which would normally be called on EDT.
* <p>
* If Toolkit posts invocation events caused by the callable, those events are intercepted and dispatched on EDT out of order.
* <p>
* When called on non-EDT, the method invokes the callable in place.
*/
public static <T> T invokeAndBlock(Callable<T> callable) {
if (callable == null) return null;
if (EventQueue.isDispatchThread()) {
InvokeOnToolkitHelper invoker = getInstance(Thread.currentThread());
if (invoker != null) {
return invoker.invoke(callable);
}
}
// fallback to default
try {
return callable.call();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@SuppressWarnings("removal")
private <T> T invoke(Callable<T> callable) {
assert EventQueue.isDispatchThread();
if (executor == null) {
// init on EDT
AccessController.doPrivileged((PrivilegedAction<?>)() ->
executor = new ThreadPoolExecutor(1, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadFactory() {
private final ThreadFactory factory = Executors.privilegedThreadFactory();
@Override
public Thread newThread(Runnable r) {
Thread t = factory.newThread(r);
t.setDaemon(true);
t.setName("AWT-InvokeOnToolkitHelper " + t.getName());
return t;
}
})
);
}
LinkedBlockingQueue<InvocationEvent> currentQueue;
synchronized (invocations) {
invocations.push(currentQueue = new LinkedBlockingQueue<>());
}
FutureTask<T> task = new FutureTask<T>(callable) {
@Override
protected void done() {
synchronized (invocations) {
// Done with the current queue, wake it up.
invocations.pop().add(new InvocationEvent(executor, () -> {}));
}
}
};
executor.execute(task);
try {
while (!task.isDone() || !currentQueue.isEmpty()) {
InvocationEvent event = currentQueue.poll(WAIT_LIMIT_SECONDS, TimeUnit.SECONDS);
if (event == null) {
new RuntimeException("Waiting for the invocation event timed out").printStackTrace();
break;
}
event.dispatch();
}
return task.isDone() ? task.get() : null;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
/**
* Warning: the method is used by the implementation and should not be used by a client.
*
* Checks if there's an active InvokeOnToolkitHelper corresponding to the invocation's AppContext,
* adds the invocation to the InvokeOnToolkitHelper's queue and returns true.
* Otherwise does nothing and returns false.
*/
public static boolean offer(InvocationEvent invocation) {
Object source = invocation.getSource();
InvokeOnToolkitHelper invoker = (source instanceof Component) ?
getInstance((Component)source) :
getInstance(Toolkit.getDefaultToolkit().getSystemEventQueue());
if (invoker == null) return false;
synchronized (invoker.invocations) {
if (!invoker.invocations.isEmpty()) {
invoker.invocations.peek().add(invocation);
return true;
}
}
return false;
}
private static InvokeOnToolkitHelper getInstance(Component comp) {
if (comp == null) return null;
AppContext appContext = SunToolkit.targetToAppContext(comp);
if (appContext == null) return null;
return getInstance((EventQueue)appContext.get(AppContext.EVENT_QUEUE_KEY));
}
private static InvokeOnToolkitHelper getInstance(EventQueue eq) {
if (eq == null) return null;
return getInstance(AWTAccessor.getEventQueueAccessor().getDispatchThread(eq));
}
private static InvokeOnToolkitHelper getInstance(Thread edt) {
if (edt == null) return null;
return edt2invokerMap.computeIfAbsent(edt, key -> new InvokeOnToolkitHelper());
}
}