Loading src/com/android/launcher3/util/RaceConditionTracker.java +3 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ package com.android.launcher3.util; public class RaceConditionTracker { public final static boolean ENTER = true; public final static boolean EXIT = false; static final String ENTER_POSTFIX = "enter"; static final String EXIT_POSTFIX = "exit"; public interface EventProcessor { void onEvent(String eventName); Loading @@ -46,7 +48,7 @@ public class RaceConditionTracker { } public static String enterExitEvt(String eventName, boolean isEnter) { return eventName + ":" + (isEnter ? "enter" : "exit"); return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX); } public static String enterEvt(String eventName) { Loading tests/src/com/android/launcher3/util/RaceConditionReproducer.java +83 −10 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.launcher3.util; import static com.android.launcher3.util.RaceConditionTracker.ENTER_POSTFIX; import static com.android.launcher3.util.RaceConditionTracker.EXIT_POSTFIX; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; Loading Loading @@ -58,6 +61,8 @@ import java.util.concurrent.TimeUnit; * Phase 3. Releasing all threads and letting the test iteration run till its end. * * The iterations end when all seen paths have been declared “uncontinuable”. * * When we register event XXX:enter, we hold all other events until we register XXX:exit. */ public class RaceConditionReproducer implements RaceConditionTracker.EventProcessor { private static final String TAG = "RaceConditionReproducer"; Loading @@ -81,7 +86,7 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces private final Map<String, EventNode> mNextEvents = new HashMap<>(); // Whether we believe that further iterations will not be able to add more events to // mNextEvents. private boolean mStoppedAddingChildren = false; private boolean mStoppedAddingChildren = true; private void debugDump(StringBuilder sb, int indent, String name) { for (int i = 0; i < indent; ++i) sb.append('.'); Loading Loading @@ -134,6 +139,8 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces } } if (!mStoppedAddingChildren) { // Mark that we have finished adding children. It will remain true if no new // children are added, or will be set to false upon adding a new child. mStoppedAddingChildren = true; return true; } Loading Loading @@ -216,6 +223,7 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces RaceConditionTracker.setEventProcessor(null); runResumeAllEventsCallbackLocked(); assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty()); assertTrue("Last registered event is :enter", lastEventAsEnter() == null); // No events came after mLastRegisteredEvent. It doesn't make sense to come to it again // because we won't see new continuations. Loading Loading @@ -245,6 +253,26 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces } } /** * Returns whether the last event was not an XXX:enter, or this event is a matching XXX:exit. */ private boolean canRegisterEventNowLocked(String event) { final String lastEventAsEnter = lastEventAsEnter(); final String thisEventAsExit = eventAsExit(event); if (lastEventAsEnter != null) { if (!lastEventAsEnter.equals(thisEventAsExit)) { assertTrue("YYY:exit after XXX:enter", thisEventAsExit == null); // Last event was :enter, but this event is not :exit. return false; } } else { // Previous event was not :enter. assertTrue(":exit after a non-enter event", thisEventAsExit == null); } return true; } /** * Registers an event issued by the app and returns null or decides that the event must be * postponed, and returns an object to wait on. Loading @@ -252,6 +280,10 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces private synchronized Semaphore tryRegisterEvent(String event) { Log.d(TAG, "Event issued by the app: " + event); if (!canRegisterEventNowLocked(event)) { return createWaitObjectForPostponedEventLocked(event); } if (mRegisteredEventCount < mSequenceToFollow.size()) { // We are in the first part of the iteration. We only register events that follow the // mSequenceToFollow and postponing all other events. Loading Loading @@ -288,9 +320,14 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces return createWaitObjectForPostponedEventLocked(event); } } else { // The second phase of the iteration. We are past the growth point and register // The third phase of the iteration. We are past the growth point and register // everything that comes. registerEventLocked(event); // Register events that may have been postponed while waiting for an :exit event // during the third phase. We don't do this if just registered event is :enter. if (eventAsEnter(event) == null && mRegisteredEventCount > mSequenceToFollow.size()) { registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet())); } } return null; } Loading Loading @@ -347,6 +384,11 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces private void registerPostponedEventsLocked(Collection<String> events) { for (String event : events) { registerPostponedEventLocked(event); if (eventAsEnter(event) != null) { // Once :enter is registered, switch to waiting for :exit to come. Won't register // other postponed events. break; } } } Loading @@ -355,14 +397,51 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces registerEventLocked(event); } /** * If the last registered event was XXX:enter, returns XXX, otherwise, null. */ private String lastEventAsEnter() { return eventAsEnter(mCurrentSequence.substring(mCurrentSequence.lastIndexOf("|") + 1)); } /** * If the event is XXX:postfix, returns XXX, otherwise, null. */ private static String prefixFromPostfixedEvent(String event, String postfix) { final int columnPos = event.indexOf(':'); if (columnPos != -1 && postfix.equals(event.substring(columnPos + 1))) { return event.substring(0, columnPos); } return null; } /** * If the event is XXX:enter, returns XXX, otherwise, null. */ private static String eventAsEnter(String event) { return prefixFromPostfixedEvent(event, ENTER_POSTFIX); } /** * If the event is XXX:exit, returns XXX, otherwise, null. */ private static String eventAsExit(String event) { return prefixFromPostfixedEvent(event, EXIT_POSTFIX); } private void registerEventLocked(String event) { assertTrue(canRegisterEventNowLocked(event)); Log.d(TAG, "Actually registering event: " + event); EventNode next = mLastRegisteredEvent.mNextEvents.get(event); if (next == null) { // This event wasn't seen after mLastRegisteredEvent. next = new EventNode(); mLastRegisteredEvent.mNextEvents.put(event, next); mLastRegisteredEvent.mStoppedAddingChildren = false; // The fact that we've added a new event after the previous one means that the // previous event is still a growth point, unless this event is :exit, which means // that the previous event is :enter. mLastRegisteredEvent.mStoppedAddingChildren = eventAsExit(event) != null; } mLastRegisteredEvent = next; Loading @@ -371,12 +450,6 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces if (mCurrentSequence.length() > 0) mCurrentSequence.append("|"); mCurrentSequence.append(event); Log.d(TAG, "Repro sequence: " + mCurrentSequence); if (mRegisteredEventCount == mSequenceToFollow.size() + 1) { // We just entered the third phase of the iteration, i.e. registered an event after // the growth point. Now we can let go of all postponed events. runResumeAllEventsCallbackLocked(); } } private void runResumeAllEventsCallbackLocked() { Loading tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java +78 −0 Original line number Diff line number Diff line Loading @@ -76,6 +76,46 @@ public class RaceConditionReproducerTest { assertTrue(sawTheValidSequence); } @Test @Ignore // The test is too long for continuous testing. // 2 threads, 3 events, including enter-exit pairs each. public void test3_3_enter_exit() throws Exception { final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(); boolean sawTheValidSequence = false; for (; ; ) { eventProcessor.startIteration(); Thread tb = new Thread(() -> { RaceConditionTracker.onEvent("B1:enter"); RaceConditionTracker.onEvent("B1:exit"); RaceConditionTracker.onEvent("B2"); RaceConditionTracker.onEvent("B3:enter"); RaceConditionTracker.onEvent("B3:exit"); }); tb.start(); RaceConditionTracker.onEvent("A1"); RaceConditionTracker.onEvent("A2:enter"); RaceConditionTracker.onEvent("A2:exit"); RaceConditionTracker.onEvent("A3:enter"); RaceConditionTracker.onEvent("A3:exit"); tb.join(); final boolean needMoreIterations = eventProcessor.finishIteration(); sawTheValidSequence = sawTheValidSequence || "B1:enter|B1:exit|A1|A2:enter|A2:exit|B2|A3:enter|A3:exit|B3:enter|B3:exit". equals(eventProcessor.getCurrentSequenceString()); if (!needMoreIterations) break; } assertEquals("Wrong number of leaf nodes", factorial(3 + 3) / (factorial(3) * factorial(3)), eventProcessor.numberOfLeafNodes()); assertTrue(sawTheValidSequence); } @Test // 2 threads, 3 events each; reproducing a particular event sequence. public void test3_3_ReproMode() throws Exception { Loading Loading @@ -122,4 +162,42 @@ public class RaceConditionReproducerTest { factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)), eventProcessor.numberOfLeafNodes()); } @Test @Ignore // The test is too long for continuous testing. // 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs. public void test2_1_2_enter_exit() throws Exception { final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(); for (; ; ) { eventProcessor.startIteration(); Thread tb = new Thread(() -> { RaceConditionTracker.onEvent("B1:enter"); RaceConditionTracker.onEvent("B1:exit"); RaceConditionTracker.onEvent("B2:enter"); RaceConditionTracker.onEvent("B2:exit"); }); tb.start(); Thread tc = new Thread(() -> { RaceConditionTracker.onEvent("C1:enter"); RaceConditionTracker.onEvent("C1:exit"); }); tc.start(); RaceConditionTracker.onEvent("A1:enter"); RaceConditionTracker.onEvent("A1:exit"); RaceConditionTracker.onEvent("A2:enter"); RaceConditionTracker.onEvent("A2:exit"); tb.join(); tc.join(); if (!eventProcessor.finishIteration()) break; } assertEquals("Wrong number of leaf nodes", factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)), eventProcessor.numberOfLeafNodes()); } } Loading
src/com/android/launcher3/util/RaceConditionTracker.java +3 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ package com.android.launcher3.util; public class RaceConditionTracker { public final static boolean ENTER = true; public final static boolean EXIT = false; static final String ENTER_POSTFIX = "enter"; static final String EXIT_POSTFIX = "exit"; public interface EventProcessor { void onEvent(String eventName); Loading @@ -46,7 +48,7 @@ public class RaceConditionTracker { } public static String enterExitEvt(String eventName, boolean isEnter) { return eventName + ":" + (isEnter ? "enter" : "exit"); return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX); } public static String enterEvt(String eventName) { Loading
tests/src/com/android/launcher3/util/RaceConditionReproducer.java +83 −10 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.launcher3.util; import static com.android.launcher3.util.RaceConditionTracker.ENTER_POSTFIX; import static com.android.launcher3.util.RaceConditionTracker.EXIT_POSTFIX; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; Loading Loading @@ -58,6 +61,8 @@ import java.util.concurrent.TimeUnit; * Phase 3. Releasing all threads and letting the test iteration run till its end. * * The iterations end when all seen paths have been declared “uncontinuable”. * * When we register event XXX:enter, we hold all other events until we register XXX:exit. */ public class RaceConditionReproducer implements RaceConditionTracker.EventProcessor { private static final String TAG = "RaceConditionReproducer"; Loading @@ -81,7 +86,7 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces private final Map<String, EventNode> mNextEvents = new HashMap<>(); // Whether we believe that further iterations will not be able to add more events to // mNextEvents. private boolean mStoppedAddingChildren = false; private boolean mStoppedAddingChildren = true; private void debugDump(StringBuilder sb, int indent, String name) { for (int i = 0; i < indent; ++i) sb.append('.'); Loading Loading @@ -134,6 +139,8 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces } } if (!mStoppedAddingChildren) { // Mark that we have finished adding children. It will remain true if no new // children are added, or will be set to false upon adding a new child. mStoppedAddingChildren = true; return true; } Loading Loading @@ -216,6 +223,7 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces RaceConditionTracker.setEventProcessor(null); runResumeAllEventsCallbackLocked(); assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty()); assertTrue("Last registered event is :enter", lastEventAsEnter() == null); // No events came after mLastRegisteredEvent. It doesn't make sense to come to it again // because we won't see new continuations. Loading Loading @@ -245,6 +253,26 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces } } /** * Returns whether the last event was not an XXX:enter, or this event is a matching XXX:exit. */ private boolean canRegisterEventNowLocked(String event) { final String lastEventAsEnter = lastEventAsEnter(); final String thisEventAsExit = eventAsExit(event); if (lastEventAsEnter != null) { if (!lastEventAsEnter.equals(thisEventAsExit)) { assertTrue("YYY:exit after XXX:enter", thisEventAsExit == null); // Last event was :enter, but this event is not :exit. return false; } } else { // Previous event was not :enter. assertTrue(":exit after a non-enter event", thisEventAsExit == null); } return true; } /** * Registers an event issued by the app and returns null or decides that the event must be * postponed, and returns an object to wait on. Loading @@ -252,6 +280,10 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces private synchronized Semaphore tryRegisterEvent(String event) { Log.d(TAG, "Event issued by the app: " + event); if (!canRegisterEventNowLocked(event)) { return createWaitObjectForPostponedEventLocked(event); } if (mRegisteredEventCount < mSequenceToFollow.size()) { // We are in the first part of the iteration. We only register events that follow the // mSequenceToFollow and postponing all other events. Loading Loading @@ -288,9 +320,14 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces return createWaitObjectForPostponedEventLocked(event); } } else { // The second phase of the iteration. We are past the growth point and register // The third phase of the iteration. We are past the growth point and register // everything that comes. registerEventLocked(event); // Register events that may have been postponed while waiting for an :exit event // during the third phase. We don't do this if just registered event is :enter. if (eventAsEnter(event) == null && mRegisteredEventCount > mSequenceToFollow.size()) { registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet())); } } return null; } Loading Loading @@ -347,6 +384,11 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces private void registerPostponedEventsLocked(Collection<String> events) { for (String event : events) { registerPostponedEventLocked(event); if (eventAsEnter(event) != null) { // Once :enter is registered, switch to waiting for :exit to come. Won't register // other postponed events. break; } } } Loading @@ -355,14 +397,51 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces registerEventLocked(event); } /** * If the last registered event was XXX:enter, returns XXX, otherwise, null. */ private String lastEventAsEnter() { return eventAsEnter(mCurrentSequence.substring(mCurrentSequence.lastIndexOf("|") + 1)); } /** * If the event is XXX:postfix, returns XXX, otherwise, null. */ private static String prefixFromPostfixedEvent(String event, String postfix) { final int columnPos = event.indexOf(':'); if (columnPos != -1 && postfix.equals(event.substring(columnPos + 1))) { return event.substring(0, columnPos); } return null; } /** * If the event is XXX:enter, returns XXX, otherwise, null. */ private static String eventAsEnter(String event) { return prefixFromPostfixedEvent(event, ENTER_POSTFIX); } /** * If the event is XXX:exit, returns XXX, otherwise, null. */ private static String eventAsExit(String event) { return prefixFromPostfixedEvent(event, EXIT_POSTFIX); } private void registerEventLocked(String event) { assertTrue(canRegisterEventNowLocked(event)); Log.d(TAG, "Actually registering event: " + event); EventNode next = mLastRegisteredEvent.mNextEvents.get(event); if (next == null) { // This event wasn't seen after mLastRegisteredEvent. next = new EventNode(); mLastRegisteredEvent.mNextEvents.put(event, next); mLastRegisteredEvent.mStoppedAddingChildren = false; // The fact that we've added a new event after the previous one means that the // previous event is still a growth point, unless this event is :exit, which means // that the previous event is :enter. mLastRegisteredEvent.mStoppedAddingChildren = eventAsExit(event) != null; } mLastRegisteredEvent = next; Loading @@ -371,12 +450,6 @@ public class RaceConditionReproducer implements RaceConditionTracker.EventProces if (mCurrentSequence.length() > 0) mCurrentSequence.append("|"); mCurrentSequence.append(event); Log.d(TAG, "Repro sequence: " + mCurrentSequence); if (mRegisteredEventCount == mSequenceToFollow.size() + 1) { // We just entered the third phase of the iteration, i.e. registered an event after // the growth point. Now we can let go of all postponed events. runResumeAllEventsCallbackLocked(); } } private void runResumeAllEventsCallbackLocked() { Loading
tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java +78 −0 Original line number Diff line number Diff line Loading @@ -76,6 +76,46 @@ public class RaceConditionReproducerTest { assertTrue(sawTheValidSequence); } @Test @Ignore // The test is too long for continuous testing. // 2 threads, 3 events, including enter-exit pairs each. public void test3_3_enter_exit() throws Exception { final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(); boolean sawTheValidSequence = false; for (; ; ) { eventProcessor.startIteration(); Thread tb = new Thread(() -> { RaceConditionTracker.onEvent("B1:enter"); RaceConditionTracker.onEvent("B1:exit"); RaceConditionTracker.onEvent("B2"); RaceConditionTracker.onEvent("B3:enter"); RaceConditionTracker.onEvent("B3:exit"); }); tb.start(); RaceConditionTracker.onEvent("A1"); RaceConditionTracker.onEvent("A2:enter"); RaceConditionTracker.onEvent("A2:exit"); RaceConditionTracker.onEvent("A3:enter"); RaceConditionTracker.onEvent("A3:exit"); tb.join(); final boolean needMoreIterations = eventProcessor.finishIteration(); sawTheValidSequence = sawTheValidSequence || "B1:enter|B1:exit|A1|A2:enter|A2:exit|B2|A3:enter|A3:exit|B3:enter|B3:exit". equals(eventProcessor.getCurrentSequenceString()); if (!needMoreIterations) break; } assertEquals("Wrong number of leaf nodes", factorial(3 + 3) / (factorial(3) * factorial(3)), eventProcessor.numberOfLeafNodes()); assertTrue(sawTheValidSequence); } @Test // 2 threads, 3 events each; reproducing a particular event sequence. public void test3_3_ReproMode() throws Exception { Loading Loading @@ -122,4 +162,42 @@ public class RaceConditionReproducerTest { factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)), eventProcessor.numberOfLeafNodes()); } @Test @Ignore // The test is too long for continuous testing. // 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs. public void test2_1_2_enter_exit() throws Exception { final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(); for (; ; ) { eventProcessor.startIteration(); Thread tb = new Thread(() -> { RaceConditionTracker.onEvent("B1:enter"); RaceConditionTracker.onEvent("B1:exit"); RaceConditionTracker.onEvent("B2:enter"); RaceConditionTracker.onEvent("B2:exit"); }); tb.start(); Thread tc = new Thread(() -> { RaceConditionTracker.onEvent("C1:enter"); RaceConditionTracker.onEvent("C1:exit"); }); tc.start(); RaceConditionTracker.onEvent("A1:enter"); RaceConditionTracker.onEvent("A1:exit"); RaceConditionTracker.onEvent("A2:enter"); RaceConditionTracker.onEvent("A2:exit"); tb.join(); tc.join(); if (!eventProcessor.finishIteration()) break; } assertEquals("Wrong number of leaf nodes", factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)), eventProcessor.numberOfLeafNodes()); } }