# HG changeset patch # User mbalao # Date 1548108259 10800 # Mon Jan 21 19:04:19 2019 -0300 # Node ID 62b4d6604ea7f09c3b69431d70b39d7521e9509a # Parent f0b93fbd8cf8b89d9d20a5314065154912b3dd36 8204142: AWT hang occurs when sequenced events arrive out of sequence in multiple AppContexts Summary: Improvements on the synchronization of SequencedEvent events from different AppContexts Reviewed-by: serb diff --git a/src/share/classes/java/awt/SequencedEvent.java b/src/share/classes/java/awt/SequencedEvent.java --- a/src/share/classes/java/awt/SequencedEvent.java +++ b/src/share/classes/java/awt/SequencedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,7 @@ package java.awt; +import java.util.Iterator; import java.util.LinkedList; import sun.awt.AWTAccessor; import sun.awt.AppContext; @@ -54,6 +55,7 @@ private final AWTEvent nested; private AppContext appContext; private boolean disposed; + private final LinkedList pendingEvents = new LinkedList<>(); static { AWTAccessor.setSequencedEventAccessor(new AWTAccessor.SequencedEventAccessor() { @@ -66,6 +68,35 @@ }); } + private static final class SequencedEventsFilter implements EventFilter { + private final SequencedEvent currentSequencedEvent; + private SequencedEventsFilter(SequencedEvent currentSequencedEvent) { + this.currentSequencedEvent = currentSequencedEvent; + } + @Override + public FilterAction acceptEvent(AWTEvent ev) { + if (ev.getID() == ID) { + // Move forward dispatching only if the event is previous + // in SequencedEvent.list. Otherwise, hold it for reposting later. + synchronized (SequencedEvent.class) { + Iterator it = list.iterator(); + while (it.hasNext()) { + SequencedEvent iev = it.next(); + if (iev.equals(currentSequencedEvent)) { + break; + } else if (iev.equals(ev)) { + return FilterAction.ACCEPT; + } + } + } + } else if (ev.getID() == SentEvent.ID) { + return FilterAction.ACCEPT; + } + currentSequencedEvent.pendingEvents.add(ev); + return FilterAction.REJECT; + } + } + /** * Constructs a new SequencedEvent which will dispatch the specified * nested event. @@ -104,11 +135,8 @@ if (EventQueue.isDispatchThread()) { EventDispatchThread edt = (EventDispatchThread) Thread.currentThread(); - edt.pumpEvents(SentEvent.ID, new Conditional() { - public boolean evaluate() { - return !SequencedEvent.this.isFirstOrDisposed(); - } - }); + edt.pumpEventsForFilter(() -> !SequencedEvent.this.isFirstOrDisposed(), + new SequencedEventsFilter(this)); } else { while(!isFirstOrDisposed()) { synchronized (SequencedEvent.class) { @@ -197,10 +225,6 @@ } disposed = true; } - // Wake myself up - if (appContext != null) { - SunToolkit.postEvent(appContext, new SentEvent()); - } SequencedEvent next = null; @@ -221,5 +245,9 @@ if (next != null && next.appContext != null) { SunToolkit.postEvent(next.appContext, new SentEvent()); } + + for(AWTEvent e : pendingEvents) { + SunToolkit.postEvent(appContext, e); + } } } diff --git a/test/java/awt/event/SequencedEvent/MultipleContextsFunctionalTest.java b/test/java/awt/event/SequencedEvent/MultipleContextsFunctionalTest.java new file mode 100644 --- /dev/null +++ b/test/java/awt/event/SequencedEvent/MultipleContextsFunctionalTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8204142 + * @summary Deadlock when queueing SequencedEvent of different AppContexts + * @author Laurent Bourges + * @modules java.desktop/sun.awt + * @run main/othervm/timeout=30 MultipleContextsFunctionalTest + */ + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; +import javax.swing.Timer; + +public final class MultipleContextsFunctionalTest { + + private static final long serialVersionUID = 1L; + + private static final int NUM_WINDOW = 2; + private static final int INTERVAL = 50; + private static final int MAX_TIME = 10000; // 10s + private static final int TOLERANCE = 10000;// 10s + private static final int CHECK_LAPSE = 100; + private static final int MAX_COUNT = MAX_TIME / INTERVAL; + private static final int EXPECTED = MAX_COUNT * NUM_WINDOW; + private static final List WINDOWS = new ArrayList(); + + public static void main(String[] args) { + for (int i = 0; i < NUM_WINDOW; i++) { + createWin(i); + } + + int total = 0; + int waitingTime = MAX_TIME + TOLERANCE; + while (waitingTime > 0 && total != EXPECTED) { + try { + Thread.sleep(CHECK_LAPSE); + } catch (InterruptedException e) { + e.printStackTrace(); + } + waitingTime -= CHECK_LAPSE; + + total = 0; + for (TestWindow window : WINDOWS) { + total += window.getCounter(); + } + } + + // Failure if AWT hanging: assert + System.out.println("Total [" + total + "] - Expected [" + EXPECTED + "]"); + if (total == EXPECTED) { + System.out.println("Test PASSED"); + return; + } + System.out.println("Test FAILED"); + Runtime.getRuntime().halt(-1); + } + + private static void createWin(int tgNum) { + new Thread(new ThreadGroup("TG " + tgNum), + new Runnable() { + @Override + public void run() { + sun.awt.SunToolkit.createNewAppContext(); + + final AtomicReference ref = + new AtomicReference(); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + final TestWindow window = new TestWindow(tgNum); + window.setVisible(true); + ref.set(window); + WINDOWS.add(window); + } + }); + + // Wait for window to show + TestWindow window = ref.get(); + while (window == null) { + try { + Thread.sleep(100); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + window = ref.get(); + } + window.enableTimer(true); + } + }).start(); + } + + private static final class TestWindow extends JFrame implements ActionListener { + + private final JButton btn; + private int counter = 0; + private final Timer t; + + TestWindow(final int num) { + super("Test Window [" + num + "]"); + setMinimumSize(new Dimension(300, 200)); + setLocation(100 + 400 * (num - 1), 100); + + setLayout(new BorderLayout()); + JLabel textBlock = new JLabel("Lorem ipsum dolor sit amet..."); + add(textBlock); + + btn = new JButton("TEST"); + add(btn, BorderLayout.SOUTH); + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + pack(); + + t = new Timer(INTERVAL, this); + t.setRepeats(false); + } + + @Override + public void actionPerformed(ActionEvent e) { + this.toFront(); + btn.setText("TEST " + (++counter)); + this.toBack(); + if (counter < MAX_COUNT) { + enableTimer(true); + } else { + dispose(); + } + } + + void enableTimer(boolean enable) { + if (enable) { + t.start(); + } else { + t.stop(); + } + } + + int getCounter() { + return counter; + } + } +} diff --git a/test/java/awt/event/SequencedEvent/MultipleContextsUnitTest.java b/test/java/awt/event/SequencedEvent/MultipleContextsUnitTest.java new file mode 100644 --- /dev/null +++ b/test/java/awt/event/SequencedEvent/MultipleContextsUnitTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.awt.AWTEvent; +import java.awt.event.InvocationEvent; +import java.lang.reflect.Constructor; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import sun.awt.AppContext; +import sun.awt.SunToolkit; + +/** + * @test + * @bug 8204142 + * @author Sergey Bylokhov + * @modules java.desktop/sun.awt + * @run main/othervm/timeout=30 MultipleContextsUnitTest + */ +public final class MultipleContextsUnitTest { + + private static final int COUNT = 20; + private static final AppContext[] apps = new AppContext[COUNT]; + private static final CountDownLatch go = new CountDownLatch(1); + private static final CountDownLatch end = new CountDownLatch(COUNT); + + private static volatile int createSENumber = 0; + private static volatile int dispatchSENumber = 0; + + public static void main(final String[] args) throws Exception { + for (int i = 0; i < COUNT; i++) { + Thread t = testThread(i); + t.start(); + t.join(); + } + + for (AppContext app : apps) { + SunToolkit.postEvent(app, new InvocationEvent(new Object(), () -> { + try { + go.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + } + + // eventOne - created first, but posted last + AWTEvent eventOne = getSequencedEvent(); + { + // eventTwo and eventThree - posted in the reverse order + AppContext app = apps[1]; + AWTEvent eventTwo = getSequencedEvent(); + AWTEvent eventThree = getSequencedEvent(); + SunToolkit.postEvent(app, eventThree); + SunToolkit.postEvent(app, eventTwo); + SunToolkit.postEvent(app, new InvocationEvent(new Object(), () -> { + System.err.println(AppContext.getAppContext()); + end.countDown(); + })); + } + + for (int i = 2; i < apps.length; i++) { + // eventTwo and eventThree - posted in the correct order + AppContext app = apps[i]; + + AWTEvent eventTwo = getSequencedEvent(); + SunToolkit.postEvent(app, eventTwo); + + AtomicReference called1 = new AtomicReference(false); + AtomicReference called2 = new AtomicReference(false); + int num1 = createSENumber; + SunToolkit.postEvent(app, new InvocationEvent(new Object(), () -> { + if (dispatchSENumber < num1) { + throw new RuntimeException("Dispatched too early"); + } + called1.set(true); + if (called2.get()) { + throw new RuntimeException("Second event is called before first"); + } + })); + AWTEvent eventThree = getSequencedEvent(); + SunToolkit.postEvent(app, eventThree); + int num2 = createSENumber; + SunToolkit.postEvent(app, new InvocationEvent(new Object(), () -> { + if (dispatchSENumber < num2) { + throw new RuntimeException("Dispatched too early"); + } + called2.set(true); + if (!called1.get()) { + throw new RuntimeException("First event is not called before second"); + } + System.err.println(AppContext.getAppContext()); + end.countDown(); + })); + } + + + + // eventOne should flush all EDT + SunToolkit.postEvent(apps[0], eventOne); + SunToolkit.postEvent(apps[0], new InvocationEvent(new Object(), () -> { + System.err.println(AppContext.getAppContext()); + end.countDown(); + })); + + go.countDown(); + + System.err.println("Start to wait"); + end.await(); + System.err.println("End to wait"); + } + + private static Thread testThread(int index) { + final ThreadGroup group = new ThreadGroup("TG " + index); + return new Thread(group, () -> { + apps[index] = SunToolkit.createNewAppContext(); + }); + } + + private static AWTEvent getSequencedEvent() + { + int num = createSENumber++; + + InvocationEvent wrapMe = new InvocationEvent(new Object(), () -> { + if (num != dispatchSENumber++) { + System.err.println("num: " + num); + System.err.println("dispatchSENumber: " + dispatchSENumber); + throw new RuntimeException("Wrong order"); + } + }); + + try { + /* + * SequencedEvent is a package private class, which cannot be instantiated + * by importing. So use reflection to create an instance. + */ + Class seqClass = (Class) Class.forName("java.awt.SequencedEvent"); + Constructor + seqConst = seqClass.getConstructor(AWTEvent.class); + seqConst.setAccessible(true); + return seqConst.newInstance(wrapMe); + } catch (Throwable err) { + throw new RuntimeException("Unable to instantiate SequencedEvent",err); + } + } +}