diff --git a/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java new file mode 100644 index 0000000000..d8aa235823 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides; + +import static android.os.IBinder.FLAG_ONEWAY; + +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.MainThread; + +import java.util.HashSet; +import java.util.Locale; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +/** + * A binder proxy transaction listener for tracking non-whitelisted binder calls. + */ +public class DejankBinderTracker implements Binder.ProxyTransactListener { + private static final String TAG = "DejankBinderTracker"; + + private static final Object sLock = new Object(); + private static final HashSet sWhitelistedFrameworkClasses = new HashSet<>(); + static { + // Common IPCs that are ok to block the main thread. + sWhitelistedFrameworkClasses.add("android.view.IWindowSession"); + sWhitelistedFrameworkClasses.add("android.os.IPowerManager"); + } + private static boolean sTemporarilyIgnoreTracking = false; + + // Used by the client to limit binder tracking to specific regions + private static boolean sTrackingAllowed = false; + + private BiConsumer mUnexpectedTransactionCallback; + private boolean mIsTracking = false; + + /** + * Temporarily ignore blocking binder calls for the duration of this {@link Runnable}. + */ + @MainThread + public static void whitelistIpcs(Runnable runnable) { + sTemporarilyIgnoreTracking = true; + runnable.run(); + sTemporarilyIgnoreTracking = false; + } + + /** + * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}. + */ + @MainThread + public static T whitelistIpcs(Supplier supplier) { + sTemporarilyIgnoreTracking = true; + T value = supplier.get(); + sTemporarilyIgnoreTracking = false; + return value; + } + + /** + * Enables binder tracking during a test. + */ + @MainThread + public static void allowBinderTrackingInTests() { + sTrackingAllowed = true; + } + + /** + * Disables binder tracking during a test. + */ + @MainThread + public static void disallowBinderTrackingInTests() { + sTrackingAllowed = false; + } + + public DejankBinderTracker(BiConsumer unexpectedTransactionCallback) { + mUnexpectedTransactionCallback = unexpectedTransactionCallback; + } + + @MainThread + public void startTracking() { + if (!Build.TYPE.toLowerCase(Locale.ROOT).contains("debug") + && !Build.TYPE.toLowerCase(Locale.ROOT).equals("eng")) { + Log.wtf(TAG, "Unexpected use of binder tracker in non-debug build", new Exception()); + return; + } + if (mIsTracking) { + return; + } + mIsTracking = true; + Binder.setProxyTransactListener(this); + } + + @MainThread + public void stopTracking() { + if (!mIsTracking) { + return; + } + mIsTracking = false; + Binder.setProxyTransactListener(null); + } + + // Override the hidden Binder#onTransactStarted method + public synchronized Object onTransactStarted(IBinder binder, int transactionCode, int flags) { + if (!mIsTracking + || !sTrackingAllowed + || sTemporarilyIgnoreTracking + || (flags & FLAG_ONEWAY) == FLAG_ONEWAY + || !isMainThread()) { + return null; + } + + String descriptor; + try { + descriptor = binder.getInterfaceDescriptor(); + if (sWhitelistedFrameworkClasses.contains(descriptor)) { + return null; + } + } catch (RemoteException e) { + e.printStackTrace(); + descriptor = binder.getClass().getSimpleName(); + } + + mUnexpectedTransactionCallback.accept(descriptor, transactionCode); + return null; + } + + @Override + public Object onTransactStarted(IBinder binder, int transactionCode) { + // Do nothing + return null; + } + + @Override + public void onTransactEnded(Object session) { + // Do nothing + } + + public static boolean isMainThread() { + return Thread.currentThread() == Looper.getMainLooper().getThread(); + } +} diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index bf4896d2cb..2b921884d2 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -112,7 +112,6 @@ import com.android.launcher3.tracing.SwipeHandlerProto; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter; import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.util.WindowBounds; @@ -588,7 +587,7 @@ public abstract class AbsSwipeUpHandler, if (mWasLauncherAlreadyVisible) { mStateCallback.setState(STATE_LAUNCHER_DRAWN); } else { - SafeCloseable traceToken = TraceHelper.INSTANCE.beginAsyncSection("WTS-init"); + Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init"); View dragLayer = activity.getDragLayer(); dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { boolean mHandled = false; @@ -600,7 +599,7 @@ public abstract class AbsSwipeUpHandler, } mHandled = true; - traceToken.close(); + TraceHelper.INSTANCE.endSection(traceToken); dragLayer.post(() -> dragLayer.getViewTreeObserver().removeOnDrawListener(this)); if (activity != mActivity) { @@ -682,10 +681,11 @@ public abstract class AbsSwipeUpHandler, private void initializeLauncherAnimationController() { buildAnimationController(); - try (SafeCloseable c = TraceHelper.INSTANCE.allowIpcs("logToggleRecents")) { - LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS, - (int) (mLauncherFrameDrawnTime - mTouchTimeMs)); - } + Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents", + TraceHelper.FLAG_IGNORE_BINDERS); + LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS, + (int) (mLauncherFrameDrawnTime - mTouchTimeMs)); + TraceHelper.INSTANCE.endSection(traceToken); // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the // high-res thumbnail loader here once we are sure that we will end up in an overview state @@ -2039,9 +2039,10 @@ public abstract class AbsSwipeUpHandler, private void setScreenshotCapturedState() { // If we haven't posted a draw callback, set the state immediately. - TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT); + Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT, + TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS); mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } private void finishCurrentTransitionToRecents() { diff --git a/quickstep/src/com/android/quickstep/BinderTracker.java b/quickstep/src/com/android/quickstep/BinderTracker.java deleted file mode 100644 index a876cd868b..0000000000 --- a/quickstep/src/com/android/quickstep/BinderTracker.java +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep; - -import static android.os.IBinder.FLAG_ONEWAY; - -import android.os.Binder; -import android.os.Binder.ProxyTransactListener; -import android.os.IBinder; -import android.os.Looper; -import android.os.RemoteException; -import android.os.Trace; -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.android.launcher3.util.SafeCloseable; -import com.android.launcher3.util.TraceHelper; - -import java.util.LinkedList; -import java.util.Set; -import java.util.function.Consumer; - -import kotlin.random.Random; - -/** - * A binder proxy transaction listener for tracking binder calls on main thread. - */ -public class BinderTracker { - - private static final String TAG = "BinderTracker"; - - // Common IPCs that are ok to block the main thread. - private static final Set sAllowedFrameworkClasses = Set.of( - "android.view.IWindowSession", - "android.os.IPowerManager", - "android.os.IServiceManager"); - - /** - * Starts tracking binder class and returns a {@link SafeCloseable} to end tracking - */ - public static SafeCloseable startTracking(Consumer callback) { - TraceHelper current = TraceHelper.INSTANCE; - - TraceHelperExtension helper = new TraceHelperExtension(callback); - TraceHelper.INSTANCE = helper; - Binder.setProxyTransactListener(helper); - - return () -> { - Binder.setProxyTransactListener(null); - TraceHelper.INSTANCE = current; - }; - } - - private static final LinkedList mMainThreadTraceStack = new LinkedList<>(); - private static final LinkedList mMainThreadIgnoreIpcStack = new LinkedList<>(); - - private static class TraceHelperExtension extends TraceHelper implements ProxyTransactListener { - - private final Consumer mUnexpectedTransactionCallback; - - TraceHelperExtension(Consumer unexpectedTransactionCallback) { - mUnexpectedTransactionCallback = unexpectedTransactionCallback; - } - - @Override - public void beginSection(String sectionName) { - if (isMainThread()) { - mMainThreadTraceStack.add(sectionName); - } - super.beginSection(sectionName); - } - - @Override - public SafeCloseable beginAsyncSection(String sectionName) { - if (!isMainThread()) { - return super.beginAsyncSection(sectionName); - } - - mMainThreadTraceStack.add(sectionName); - int cookie = Random.Default.nextInt(); - Trace.beginAsyncSection(sectionName, cookie); - return () -> { - Trace.endAsyncSection(sectionName, cookie); - mMainThreadTraceStack.remove(sectionName); - }; - } - - @Override - public void endSection() { - super.endSection(); - if (isMainThread()) { - mMainThreadTraceStack.pollLast(); - } - } - - @Override - public SafeCloseable allowIpcs(String rpcName) { - if (!isMainThread()) { - return super.allowIpcs(rpcName); - } - - mMainThreadTraceStack.add(rpcName); - mMainThreadIgnoreIpcStack.add(rpcName); - int cookie = Random.Default.nextInt(); - Trace.beginAsyncSection(rpcName, cookie); - return () -> { - Trace.endAsyncSection(rpcName, cookie); - mMainThreadTraceStack.remove(rpcName); - mMainThreadIgnoreIpcStack.remove(rpcName); - }; - } - - @Override - public Object onTransactStarted(IBinder binder, int transactionCode, int flags) { - if (!isMainThread() || (flags & FLAG_ONEWAY) == FLAG_ONEWAY) { - return null; - } - - String ipcBypass = mMainThreadIgnoreIpcStack.peekLast(); - String descriptor; - try { - descriptor = binder.getInterfaceDescriptor(); - if (sAllowedFrameworkClasses.contains(descriptor)) { - return null; - } - } catch (RemoteException e) { - Log.e(TAG, "Error getting IPC descriptor", e); - descriptor = binder.getClass().getSimpleName(); - } - - if (ipcBypass == null) { - mUnexpectedTransactionCallback.accept(new BinderCallSite( - mMainThreadTraceStack.peekLast(), descriptor, transactionCode)); - } else { - Log.d(TAG, "MainThread-IPC " + descriptor + " ignored due to " + ipcBypass); - } - return null; - } - - @Override - public Object onTransactStarted(IBinder binder, int transactionCode) { - // Do nothing - return null; - } - - @Override - public void onTransactEnded(Object session) { - // Do nothing - } - } - - private static boolean isMainThread() { - return Thread.currentThread() == Looper.getMainLooper().getThread(); - } - - /** - * Information about a binder call - */ - public static class BinderCallSite { - - @Nullable - public final String activeTrace; - public final String descriptor; - public final int transactionCode; - - BinderCallSite(String activeTrace, String descriptor, int transactionCode) { - this.activeTrace = activeTrace; - this.descriptor = descriptor; - this.transactionCode = transactionCode; - } - } -} diff --git a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java index 529213c5de..7638541023 100644 --- a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java +++ b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java @@ -16,13 +16,10 @@ package com.android.quickstep; -import android.app.ActivityThread; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.os.RemoteException; -import android.util.Log; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.util.InstantAppResolver; @@ -52,14 +49,4 @@ public class InstantAppResolverImpl extends InstantAppResolver { ComponentName cn = info.getTargetComponent(); return cn != null && cn.getClassName().equals(COMPONENT_CLASS_MARKER); } - - @Override - public boolean isInstantApp(String packageName, int userId) { - try { - return ActivityThread.getPackageManager().isInstantApp(packageName, userId); - } catch (RemoteException e) { - Log.e(TAG, "Failed to determine whether package is instant app " + packageName, e); - return false; - } - } } diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java index 128b0451c3..5f589bfa50 100644 --- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java +++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java @@ -60,10 +60,5 @@ public class QuickstepProcessInitializer extends MainProcessInitializer { // Elevate GPU priority for Quickstep and Remote animations. ThreadedRenderer.setContextPriority( ThreadedRenderer.EGL_CONTEXT_PRIORITY_HIGH_IMG); - - if (BuildConfig.IS_STUDIO_BUILD) { - BinderTracker.startTracking(call -> Log.e("BinderCall", - call.descriptor + " called on mainthread under " + call.activeTrace)); - } } } diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java index 810c028341..813523888d 100644 --- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java +++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java @@ -393,12 +393,11 @@ public interface TaskShortcutFactory { @Override public List getShortcuts(BaseDraggingActivity activity, TaskIdAttributeContainer taskContainer) { - Task t = taskContainer.getTask(); - return InstantAppResolver.newInstance(activity).isInstantApp( - t.getTopComponent().getPackageName(), t.getKey().userId) - ? Collections.singletonList(new SystemShortcut.Install(activity, - taskContainer.getItemInfo(), taskContainer.getTaskView())) - : null; + return InstantAppResolver.newInstance(activity).isInstantApp(activity, + taskContainer.getTask().getTopComponent().getPackageName()) ? + Collections.singletonList(new SystemShortcut.Install(activity, + taskContainer.getItemInfo(), taskContainer.getTaskView())) : + null; } }; diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java index 80a449bf5b..67360c4cc3 100644 --- a/quickstep/src/com/android/quickstep/TaskUtils.java +++ b/quickstep/src/com/android/quickstep/TaskUtils.java @@ -33,7 +33,6 @@ import androidx.annotation.Nullable; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageManagerHelper; -import com.android.launcher3.util.TraceHelper; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -52,8 +51,7 @@ public final class TaskUtils { * TODO: remove this once we switch to getting the icon and label from IconCache. */ public static CharSequence getTitle(Context context, Task task) { - return TraceHelper.allowIpcs("TaskUtils.getTitle", () -> - getTitle(context, task.key.userId, task.getTopComponent().getPackageName())); + return getTitle(context, task.key.userId, task.getTopComponent().getPackageName()); } public static CharSequence getTitle( diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index af57172cdb..2a1cc7032c 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -102,7 +102,6 @@ import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.OnboardingPrefs; -import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.inputconsumers.AccessibilityInputConsumer; import com.android.quickstep.inputconsumers.AssistantInputConsumer; @@ -701,7 +700,8 @@ public class TouchInteractionService extends Service return; } - SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent"); + Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride( + TraceHelper.FLAG_ALLOW_BINDER_TRACKING); final int action = event.getActionMasked(); // Note this will create a new consumer every mouse click, as after ACTION_UP from the click @@ -797,7 +797,7 @@ public class TouchInteractionService extends Service if (cleanUpConsumer) { reset(); } - traceToken.close(); + TraceHelper.INSTANCE.endFlagsOverride(traceToken); ProtoTracer.INSTANCE.get(this).scheduleFrameUpdate(); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java index 10c6316ac1..5b27f9bde1 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java @@ -28,6 +28,7 @@ import static com.android.launcher3.PagedView.DEBUG_FAILED_QUICKSWITCH; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; import static com.android.launcher3.Utilities.squaredHypot; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS; import static com.android.launcher3.util.VelocityUtils.PX_PER_MS; import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; @@ -228,7 +229,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC // Until we detect the gesture, handle events as we receive them mInputEventReceiver.setBatchingEnabled(false); - TraceHelper.INSTANCE.beginSection(DOWN_EVT); + Object traceToken = TraceHelper.INSTANCE.beginSection(DOWN_EVT, + FLAG_CHECK_FOR_RACE_CONDITIONS); mActivePointerId = ev.getPointerId(0); mDownPos.set(ev.getX(), ev.getY()); mLastPos.set(mDownPos); @@ -239,7 +241,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC startTouchTrackingForWindowAnimation(ev.getEventTime()); } - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); break; } case ACTION_POINTER_DOWN: { @@ -415,7 +417,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC * the animation can still be running. */ private void finishTouchTracking(MotionEvent ev) { - TraceHelper.INSTANCE.beginSection(UP_EVT); + Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT, + FLAG_CHECK_FOR_RACE_CONDITIONS); if (mPassedWindowMoveSlop && mInteractionHandler != null) { if (ev.getActionMasked() == ACTION_CANCEL) { @@ -452,7 +455,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC onInteractionGestureFinished(); } cleanupAfterGesture(); - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } private void cleanupAfterGesture() { diff --git a/quickstep/src/com/android/quickstep/util/BinderTracker.java b/quickstep/src/com/android/quickstep/util/BinderTracker.java new file mode 100644 index 0000000000..cb04e5b015 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/BinderTracker.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import android.os.Binder; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; + +import com.android.launcher3.config.FeatureFlags; + +/** + * Utility class to test and check binder calls during development. + */ +public class BinderTracker { + + private static final String TAG = "BinderTracker"; + + public static void start() { + if (!FeatureFlags.IS_STUDIO_BUILD) { + Log.wtf(TAG, "Accessing tracker in released code.", new Exception()); + return; + } + + Binder.setProxyTransactListener(new Tracker()); + } + + public static void stop() { + if (!FeatureFlags.IS_STUDIO_BUILD) { + Log.wtf(TAG, "Accessing tracker in released code.", new Exception()); + return; + } + Binder.setProxyTransactListener(null); + } + + private static class Tracker implements Binder.ProxyTransactListener { + + @Override + public Object onTransactStarted(IBinder iBinder, int code) { + if (Looper.myLooper() == Looper.getMainLooper()) { + Log.e(TAG, "Binder call on ui thread", new Exception()); + } + return null; + } + + @Override + public void onTransactEnded(Object session) { } + } +} diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 7f035a2b5a..f8893bd9dd 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -112,7 +112,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mContext = context; mSizeStrategy = sizeStrategy; - mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init", + // TODO(b/187074722): Don't create this per-TaskViewSimulator + mOrientationState = TraceHelper.allowIpcs("", () -> new RecentsOrientedState(context, sizeStrategy, i -> { })); mOrientationState.setGestureActive(true); mCurrentFullscreenParams = new FullscreenDrawParams(context); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index 46dd94b687..200252af88 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -32,7 +32,6 @@ import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; -import static com.android.quickstep.TaskOverlayFactory.getEnabledShortcuts; import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -89,7 +88,6 @@ import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; -import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.TransformingTouchDelegate; import com.android.launcher3.util.ViewPool.Reusable; import com.android.quickstep.RecentsModel; @@ -1558,8 +1556,8 @@ public class TaskView extends FrameLayout implements Reusable { if (taskContainer == null) { continue; } - for (SystemShortcut s : TraceHelper.allowIpcs( - "TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) { + for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this, + taskContainer)) { info.addAction(s.createAccessibilityAction(context)); } } @@ -1596,7 +1594,7 @@ public class TaskView extends FrameLayout implements Reusable { if (taskContainer == null) { continue; } - for (SystemShortcut s : getEnabledShortcuts(this, + for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this, taskContainer)) { if (s.hasHandlerForAction(action)) { s.onClick(this); diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java index 5127190cec..df5303f4f4 100644 --- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java +++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java @@ -20,6 +20,7 @@ import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.ui.TaplTestsLauncher3; +import com.android.launcher3.util.RaceConditionReproducer; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import org.junit.Before; @@ -44,6 +45,18 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest { startTestActivity(2); } + private void runTest(String... eventSequence) { + final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(eventSequence); + + // Destroy Launcher activity. + closeLauncherActivity(); + + // The test action. + eventProcessor.startIteration(); + mLauncher.goHome(); + eventProcessor.finishIteration(); + } + @Ignore @Test @NavigationModeSwitch diff --git a/res/drawable/ic_wallpaper.xml b/res/drawable/ic_wallpaper.xml new file mode 100644 index 0000000000..9543f88192 --- /dev/null +++ b/res/drawable/ic_wallpaper.xml @@ -0,0 +1,27 @@ + + + + diff --git a/res/values/config.xml b/res/values/config.xml index 83f840d0b2..5a6698b80e 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -102,6 +102,8 @@ + + com.android.customization.picker.CustomizationPickerActivity diff --git a/res/values/strings.xml b/res/values/strings.xml index 1b46b4dba8..c2eb3735af 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -250,6 +250,8 @@ + Wallpapers + Wallpaper & style Edit Home Screen diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 8071ae41ef..1da0ef407e 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -445,7 +445,8 @@ public class Launcher extends StatefulActivity Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME, DISPLAY_ALL_APPS_TRACE_COOKIE); } - TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT); + Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT, + TraceHelper.FLAG_UI_EVENT); if (DEBUG_STRICT_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() @@ -579,7 +580,7 @@ public class Launcher extends StatefulActivity LauncherOverlayPlugin.class, false /* allowedMultiple */); mRotationHelper.initialize(); - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); mUserChangedCallbackCloseable = UserCache.INSTANCE.get(this).addUserChangeListener( () -> getStateManager().goToState(NORMAL)); @@ -1077,14 +1078,15 @@ public class Launcher extends StatefulActivity @Override protected void onStart() { - TraceHelper.INSTANCE.beginSection(ON_START_EVT); + Object traceToken = TraceHelper.INSTANCE.beginSection(ON_START_EVT, + TraceHelper.FLAG_UI_EVENT); super.onStart(); if (!mDeferOverlayCallbacks) { mOverlayManager.onActivityStarted(this); } mAppWidgetHolder.setActivityStarted(true); - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } @Override @@ -1255,7 +1257,8 @@ public class Launcher extends StatefulActivity @Override protected void onResume() { - TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT); + Object traceToken = TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT, + TraceHelper.FLAG_UI_EVENT); super.onResume(); if (mDeferOverlayCallbacks) { @@ -1265,7 +1268,7 @@ public class Launcher extends StatefulActivity } DragView.removeAllViews(this); - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } @Override @@ -1653,7 +1656,7 @@ public class Launcher extends StatefulActivity if (Utilities.isRunningInTestHarness()) { Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Launcher.onNewIntent: " + intent); } - TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT); + Object traceToken = TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT); super.onNewIntent(intent); boolean alreadyOnHome = hasWindowFocus() && ((intent.getFlags() & @@ -1700,7 +1703,7 @@ public class Launcher extends StatefulActivity showAllAppsWorkTabFromIntent(alreadyOnHome); } - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } protected void toggleAllAppsFromIntent(boolean alreadyOnHome) { @@ -2299,7 +2302,7 @@ public class Launcher extends StatefulActivity * Implementation of the method from LauncherModel.Callbacks. */ public void startBinding() { - TraceHelper.INSTANCE.beginSection("startBinding"); + Object traceToken = TraceHelper.INSTANCE.beginSection("startBinding"); // Floating panels (except the full widget sheet) are associated with individual icons. If // we are starting a fresh bind, close all such panels as all the icons are about // to go away. @@ -2317,7 +2320,7 @@ public class Launcher extends StatefulActivity if (mHotseat != null) { mHotseat.resetLayout(getDeviceProfile().isVerticalBarLayout()); } - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } @Override @@ -2570,7 +2573,7 @@ public class Launcher extends StatefulActivity return view; } - TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId); + Object traceToken = TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId); try { final LauncherAppWidgetProviderInfo appWidgetInfo; @@ -2700,7 +2703,7 @@ public class Launcher extends StatefulActivity } prepareAppWidget(view, item); } finally { - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } return view; @@ -2793,7 +2796,7 @@ public class Launcher extends StatefulActivity * Implementation of the method from LauncherModel.Callbacks. */ public void finishBindingItems(IntSet pagesBoundFirst) { - TraceHelper.INSTANCE.beginSection("finishBindingItems"); + Object traceToken = TraceHelper.INSTANCE.beginSection("finishBindingItems"); mWorkspace.restoreInstanceStateForRemainingPages(); setWorkspaceLoading(false); @@ -2818,7 +2821,7 @@ public class Launcher extends StatefulActivity mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows); getViewCache().setCacheSize(R.layout.folder_page, 2); - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); mWorkspace.removeExtraEmptyScreen(true); } diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 3eb2be8364..91fd65d056 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -203,7 +203,7 @@ public class LoaderTask implements Runnable { } } - TraceHelper.INSTANCE.beginSection(TAG); + Object traceToken = TraceHelper.INSTANCE.beginSection(TAG); LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger(); try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) { List allShortcuts = new ArrayList<>(); @@ -327,7 +327,7 @@ public class LoaderTask implements Runnable { memoryLogger.printLogs(); throw e; } - TraceHelper.INSTANCE.endSection(); + TraceHelper.INSTANCE.endSection(traceToken); } public synchronized void stopLocked() { diff --git a/src/com/android/launcher3/util/InstantAppResolver.java b/src/com/android/launcher3/util/InstantAppResolver.java index bdb5e775ad..6f706d2be1 100644 --- a/src/com/android/launcher3/util/InstantAppResolver.java +++ b/src/com/android/launcher3/util/InstantAppResolver.java @@ -42,7 +42,14 @@ public class InstantAppResolver implements ResourceBasedOverride { return false; } - public boolean isInstantApp(String packageName, int userId) { + public boolean isInstantApp(Context context, String packageName) { + PackageManager packageManager = context.getPackageManager(); + try { + return isInstantApp(packageManager.getPackageInfo(packageName, 0).applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + Log.e("InstantAppResolver", "Failed to determine whether package is instant app " + + packageName, e); + } return false; } } diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java index 91203a7f9b..1d6bc253ec 100644 --- a/src/com/android/launcher3/util/PackageManagerHelper.java +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -164,6 +164,13 @@ public class PackageManagerHelper { } } + public static Intent getStyleWallpapersIntent(Context context) { + return new Intent(Intent.ACTION_SET_WALLPAPER).setComponent( + new ComponentName(context.getString(R.string.wallpaper_picker_package), + context.getString(R.string.custom_activity_picker) + )); + } + /** * Starts the details activity for {@code info} */ diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java index d5056eea15..c23df77db1 100644 --- a/src/com/android/launcher3/util/TraceHelper.java +++ b/src/com/android/launcher3/util/TraceHelper.java @@ -21,8 +21,6 @@ import androidx.annotation.MainThread; import java.util.function.Supplier; -import kotlin.random.Random; - /** * A wrapper around {@link Trace} to allow better testing. * @@ -38,53 +36,54 @@ public class TraceHelper { // Temporarily ignore blocking binder calls for this trace. public static final int FLAG_IGNORE_BINDERS = 1 << 1; + public static final int FLAG_CHECK_FOR_RACE_CONDITIONS = 1 << 2; + + public static final int FLAG_UI_EVENT = + FLAG_ALLOW_BINDER_TRACKING | FLAG_CHECK_FOR_RACE_CONDITIONS; + /** * Static instance of Trace helper, overridden in tests. */ public static TraceHelper INSTANCE = new TraceHelper(); /** - * @see Trace#beginSection(String) + * @return a token to pass into {@link #endSection(Object)}. */ - public void beginSection(String sectionName) { + public Object beginSection(String sectionName) { + return beginSection(sectionName, 0); + } + + public Object beginSection(String sectionName, int flags) { Trace.beginSection(sectionName); + return null; } /** - * @see Trace#endSection() + * @param token the token returned from {@link #beginSection(String, int)} */ - public void endSection() { + public void endSection(Object token) { Trace.endSection(); } /** - * @see Trace#beginAsyncSection(String, int) - * @return a SafeCloseable that can be used to end the session + * Similar to {@link #beginSection} but doesn't add a trace section. */ - public SafeCloseable beginAsyncSection(String sectionName) { - int cookie = Random.Default.nextInt(); - Trace.beginAsyncSection(sectionName, cookie); - return () -> Trace.endAsyncSection(sectionName, cookie); + public Object beginFlagsOverride(int flags) { + return null; } - /** - * Returns a SafeCloseable to temporarily ignore blocking binder calls. - */ - public SafeCloseable allowIpcs(String rpcName) { - int cookie = Random.Default.nextInt(); - Trace.beginAsyncSection(rpcName, cookie); - return () -> Trace.endAsyncSection(rpcName, cookie); - } + public void endFlagsOverride(Object token) { } /** * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}. - * - * Note, new features should be designed to not rely on mainThread RPCs. */ @MainThread public static T allowIpcs(String rpcName, Supplier supplier) { - try (SafeCloseable c = INSTANCE.allowIpcs(rpcName)) { + Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS); + try { return supplier.get(); + } finally { + INSTANCE.endSection(traceToken); } } } diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java index 55febc7352..aebf752941 100644 --- a/src/com/android/launcher3/views/OptionsPopupView.java +++ b/src/com/android/launcher3/views/OptionsPopupView.java @@ -55,6 +55,7 @@ import com.android.launcher3.popup.ArrowPopup; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.widget.picker.WidgetsFullSheet; import java.util.ArrayList; @@ -189,9 +190,14 @@ public class OptionsPopupView extends ArrowPopup */ public static ArrayList getOptions(Launcher launcher) { ArrayList options = new ArrayList<>(); + boolean styleWallpaperExists = styleWallpapersExists(launcher); + int resString = styleWallpaperExists + ? R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text; + int resDrawable = styleWallpaperExists + ? R.drawable.ic_palette : R.drawable.ic_wallpaper; options.add(new OptionItem(launcher, - R.string.styles_wallpaper_button_text, - R.drawable.ic_palette, + resString, + resDrawable, IGNORE, OptionsPopupView::startWallpaperPicker)); if (!WidgetsModel.GO_DISABLE_WIDGETS) { @@ -268,8 +274,12 @@ public class OptionsPopupView extends ArrowPopup .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) .putExtra(EXTRA_WALLPAPER_OFFSET, launcher.getWorkspace().getWallpaperOffsetForCenterPage()) - .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher") - .putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper"); + .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher"); + if (!styleWallpapersExists(launcher)) { + intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "wallpaper_only"); + } else { + intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper"); + } String pickerPackage = launcher.getString(R.string.wallpaper_picker_package); if (!TextUtils.isEmpty(pickerPackage)) { intent.setPackage(pickerPackage); @@ -312,4 +322,9 @@ public class OptionsPopupView extends ArrowPopup this.clickListener = clickListener; } } + + private static boolean styleWallpapersExists(Context context) { + return context.getPackageManager().resolveActivity( + PackageManagerHelper.getStyleWallpapersIntent(context), 0) != null; + } } diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java new file mode 100644 index 0000000000..ed2ec7b40a --- /dev/null +++ b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import static com.android.launcher3.util.Executors.createAndStartNewLooper; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Handler; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * Event processor for reliably reproducing multithreaded apps race conditions in tests. + * + * The app notifies us about “events” that happen in its threads. The race condition test runs the + * test action multiple times (aka iterations), trying to generate all possible permutations of + * these events. It keeps a set of all seen event sequences and steers the execution towards + * executing events in previously unseen order. It does it by postponing execution of threads that + * would lead to an already seen sequence. + * + * If an event A occurs before event B in the sequence, this is how execution order looks like: + * Events: ... A ... B ... + * Events and instructions, guaranteed order: + * (instructions executed prior to A) A ... B (instructions executed after B) + * + * Each iteration has 3 parts (phases). + * Phase 1. Picking a previously seen event subsequence that we believe can have previously unseen + * continuations. Reproducing this sequence by pausing threads that would lead to other sequences. + * Phase 2. Trying to generate previously unseen continuation of the sequence from Phase 1. We need + * one new event after that sequence. All threads leading to seen continuations will be postponed + * for some short period of time. The phase ends once the new event is registered, or after the + * period of time ends (in which case we declare that the sequence can’t have new continuations). + * 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 { + private static final String TAG = "RaceConditionReproducer"; + + private static final boolean ENTER = true; + private static final boolean EXIT = false; + private static final String ENTER_POSTFIX = "enter"; + private static final String EXIT_POSTFIX = "exit"; + + private static final long SHORT_TIMEOUT_MS = 2000; + private static final long LONG_TIMEOUT_MS = 60000; + // Handler used to resume postponed events. + private static final Handler POSTPONED_EVENT_RESUME_HANDLER = + new Handler(createAndStartNewLooper("RaceConditionEventResumer")); + + public static String enterExitEvt(String eventName, boolean isEnter) { + return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX); + } + + public static String enterEvt(String eventName) { + return enterExitEvt(eventName, ENTER); + } + + public static String exitEvt(String eventName) { + return enterExitEvt(eventName, EXIT); + } + + /** + * Event in a particular sequence of events. A node in the prefix tree of all seen event + * sequences. + */ + private class EventNode { + // Events that were seen just after this event. + private final Map mNextEvents = new HashMap<>(); + // Whether we believe that further iterations will not be able to add more events to + // mNextEvents. + private boolean mStoppedAddingChildren = true; + + private void debugDump(StringBuilder sb, int indent, String name) { + for (int i = 0; i < indent; ++i) sb.append('.'); + sb.append(!mStoppedAddingChildren ? "+" : "-"); + sb.append(" : "); + sb.append(name); + if (mLastRegisteredEvent == this) sb.append(" <"); + sb.append('\n'); + + for (String key : mNextEvents.keySet()) { + mNextEvents.get(key).debugDump(sb, indent + 2, key); + } + } + + /** Number of leaves in the subtree with this node as a root. */ + private int numberOfLeafNodes() { + if (mNextEvents.isEmpty()) return 1; + + int leaves = 0; + for (String event : mNextEvents.keySet()) { + leaves += mNextEvents.get(event).numberOfLeafNodes(); + } + return leaves; + } + + /** + * Whether we believe that further iterations will not be able add nodes to the subtree with + * this node as a root. + */ + private boolean stoppedAddingChildrenToTree() { + if (!mStoppedAddingChildren) return false; + + for (String event : mNextEvents.keySet()) { + if (!mNextEvents.get(event).stoppedAddingChildrenToTree()) return false; + } + return true; + } + + /** + * In the subtree with this node as a root, tries finding a node where we may have a + * chance to add new children. + * If succeeds, returns true and fills 'path' with the sequence of events to that node; + * otherwise returns false. + */ + private boolean populatePathToGrowthPoint(List path) { + for (String event : mNextEvents.keySet()) { + if (mNextEvents.get(event).populatePathToGrowthPoint(path)) { + path.add(0, event); + return true; + } + } + 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; + } + return false; + } + } + + // Starting point of all event sequences; the root of the prefix tree representation all + // sequences generated by test iterations. A test iteration can add nodes int it. + private EventNode mRoot = new EventNode(); + // During a test iteration, the last event that was registered. + private EventNode mLastRegisteredEvent; + // Length of the current sequence of registered events for the current test iteration. + private int mRegisteredEventCount = 0; + // During the first part of a test iteration, we go to a specific node under mRoot by + // 'playing back' mSequenceToFollow. During this part, all events that don't belong to this + // sequence get postponed. + private List mSequenceToFollow = new ArrayList<>(); + // Collection of events that got postponed, with corresponding wait objects used to let them go. + private Map mPostponedEvents = new HashMap<>(); + // Callback to run by POSTPONED_EVENT_RESUME_HANDLER, used to let go of all currently + // postponed events. + private Runnable mResumeAllEventsCallback; + // String representation of the sequence of events registered so far for the current test + // iteration. After registering any event, we output it to the log. The last output before + // the test failure can be later played back to reliable reproduce the exact sequence of + // events that broke the test. + // Format: EV1|EV2|...\EVN + private StringBuilder mCurrentSequence; + // When not null, we are in a repro mode. We run only one test iteration, and are trying to + // reproduce the event sequence represented by this string. The format is same as for + // mCurrentSequence. + private final String mReproString; + + /* Constructor for a normal test. */ + public RaceConditionReproducer() { + mReproString = null; + } + + /** + * Constructor for reliably reproducing a race condition failure. The developer should find in + * the log the latest "Repro sequence:" record and locally modify the test by passing that + * string to the constructor. Running the test will have only one iteration that will reliably + * "play back" that sequence. + */ + public RaceConditionReproducer(String reproString) { + mReproString = reproString; + } + + public RaceConditionReproducer(String... reproSequence) { + this(String.join("|", reproSequence)); + } + + public synchronized String getCurrentSequenceString() { + return mCurrentSequence.toString(); + } + + /** + * Starts a new test iteration. Events reported via RaceConditionTracker.onEvent before this + * call will be ignored. + */ + public synchronized void startIteration() { + mLastRegisteredEvent = mRoot; + mRegisteredEventCount = 0; + mCurrentSequence = new StringBuilder(); + Log.d(TAG, "Repro sequence: " + mCurrentSequence); + mSequenceToFollow = mReproString != null ? + parseReproString(mReproString) : generateSequenceToFollowLocked(); + Log.e(TAG, "---- Start of iteration; state:\n" + dumpStateLocked()); + checkIfCompletedSequenceToFollowLocked(); + + TraceHelperForTest.setRaceConditionReproducer(this); + } + + /** + * Ends a new test iteration. Events reported via RaceConditionTracker.onEvent after this call + * will be ignored. + * Returns whether we need more iterations. + */ + public synchronized boolean finishIteration() { + TraceHelperForTest.setRaceConditionReproducer(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. + mLastRegisteredEvent.mStoppedAddingChildren = true; + Log.e(TAG, "---- End of iteration; state:\n" + dumpStateLocked()); + if (mReproString != null) { + assertTrue("Repro mode: failed to reproduce the sequence", + mCurrentSequence.toString().startsWith(mReproString)); + } + // If we are in a repro mode, we need only one iteration. Otherwise, continue if the tree + // has prospective growth points. + return mReproString == null && !mRoot.stoppedAddingChildrenToTree(); + } + + private static List parseReproString(String reproString) { + return Arrays.asList(reproString.split("\\|")); + } + + /** + * Called when the app issues an event. + */ + public void onEvent(String event) { + final Semaphore waitObject = tryRegisterEvent(event); + if (waitObject != null) { + waitUntilCanRegister(event, waitObject); + } + } + + /** + * 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. + */ + 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. + if (event.equals(mSequenceToFollow.get(mRegisteredEventCount))) { + // The event is the next one expected in the sequence. Register it. + registerEventLocked(event); + + // If there are postponed events that could continue the sequence, register them. + while (mRegisteredEventCount < mSequenceToFollow.size() && + mPostponedEvents.containsKey( + mSequenceToFollow.get(mRegisteredEventCount))) { + registerPostponedEventLocked(mSequenceToFollow.get(mRegisteredEventCount)); + } + + // Perhaps we just completed the required sequence... + checkIfCompletedSequenceToFollowLocked(); + } else { + // The event is not the next one in the sequence. Postpone it. + return createWaitObjectForPostponedEventLocked(event); + } + } else if (mRegisteredEventCount == mSequenceToFollow.size()) { + // The second phase of the iteration. We have just registered the whole + // mSequenceToFollow, and want to add previously not seen continuations for the last + // node in the sequence aka 'growth point'. + if (!mLastRegisteredEvent.mNextEvents.containsKey(event) || mReproString != null) { + // The event was never seen as a continuation for the current node. + // Or we are in repro mode, in which case we are not in business of generating + // new sequences after we've played back the required sequence. + // Register it immediately. + registerEventLocked(event); + } else { + // The event was seen as a continuation for the current node. Postpone it, hoping + // that a new event will come from other threads. + return createWaitObjectForPostponedEventLocked(event); + } + } else { + // 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; + } + + /** Called when there are chances that we just have registered the whole mSequenceToFollow. */ + private void checkIfCompletedSequenceToFollowLocked() { + if (mRegisteredEventCount == mSequenceToFollow.size()) { + // We just entered the second phase of the iteration. We have just registered the + // whole mSequenceToFollow, and want to add previously not seen continuations for the + // last node in the sequence aka 'growth point'. All seen continuations will be + // postponed for SHORT_TIMEOUT_MS. At the end of this time period, we'll let them go. + scheduleResumeAllEventsLocked(); + + // Among the events that were postponed during the first stage, there may be an event + // that wasn't seen after the current. If so, register it immediately because this + // creates a new sequence. + final Set keys = new HashSet<>(mPostponedEvents.keySet()); + keys.removeAll(mLastRegisteredEvent.mNextEvents.keySet()); + if (!keys.isEmpty()) { + registerPostponedEventLocked(keys.iterator().next()); + } + } + } + + private Semaphore createWaitObjectForPostponedEventLocked(String event) { + final Semaphore waitObject = new Semaphore(0); + assertTrue("Event already postponed: " + event, !mPostponedEvents.containsKey(event)); + mPostponedEvents.put(event, waitObject); + return waitObject; + } + + private void waitUntilCanRegister(String event, Semaphore waitObject) { + try { + assertTrue("Never registered event: " + event, + waitObject.tryAcquire(LONG_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + fail("Wait was interrupted"); + } + } + + /** Schedules resuming all postponed events after SHORT_TIMEOUT_MS */ + private void scheduleResumeAllEventsLocked() { + assertTrue(mResumeAllEventsCallback == null); + mResumeAllEventsCallback = this::allEventsResumeCallback; + POSTPONED_EVENT_RESUME_HANDLER.postDelayed(mResumeAllEventsCallback, SHORT_TIMEOUT_MS); + } + + private synchronized void allEventsResumeCallback() { + assertTrue("In callback, but callback is not set", mResumeAllEventsCallback != null); + mResumeAllEventsCallback = null; + registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet())); + } + + private void registerPostponedEventsLocked(Collection 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; + } + } + } + + private void registerPostponedEventLocked(String event) { + mPostponedEvents.remove(event).release(); + 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); + // 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; + mRegisteredEventCount++; + + if (mCurrentSequence.length() > 0) mCurrentSequence.append("|"); + mCurrentSequence.append(event); + Log.d(TAG, "Repro sequence: " + mCurrentSequence); + } + + private void runResumeAllEventsCallbackLocked() { + if (mResumeAllEventsCallback != null) { + POSTPONED_EVENT_RESUME_HANDLER.removeCallbacks(mResumeAllEventsCallback); + mResumeAllEventsCallback.run(); + } + } + + private CharSequence dumpStateLocked() { + StringBuilder sb = new StringBuilder(); + + sb.append("Sequence to follow: "); + for (String event : mSequenceToFollow) sb.append(" " + event); + sb.append(".\n"); + sb.append("Registered event count: " + mRegisteredEventCount); + + sb.append("\nPostponed events: "); + for (String event : mPostponedEvents.keySet()) sb.append(" " + event); + sb.append("."); + + sb.append("\nNodes: \n"); + mRoot.debugDump(sb, 0, ""); + return sb; + } + + public int numberOfLeafNodes() { + return mRoot.numberOfLeafNodes(); + } + + private List generateSequenceToFollowLocked() { + ArrayList sequence = new ArrayList<>(); + mRoot.populatePathToGrowthPoint(sequence); + return sequence; + } +} diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java new file mode 100644 index 0000000000..59f21734f4 --- /dev/null +++ b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class RaceConditionReproducerTest { + private final static String SOME_VALID_SEQUENCE_3_3 = "B1|A1|A2|B2|A3|B3"; + + private static int factorial(int n) { + int res = 1; + for (int i = 2; i <= n; ++i) res *= i; + return res; + } + + RaceConditionReproducer eventProcessor; + + @Before + public void setup() { + eventProcessor = new RaceConditionReproducer(); + } + + @After + public void tearDown() { + TraceHelperForTest.cleanup(); + } + + private void run3_3_TestAction() throws InterruptedException { + Thread tb = new Thread(() -> { + eventProcessor.onEvent("B1"); + eventProcessor.onEvent("B2"); + eventProcessor.onEvent("B3"); + }); + tb.start(); + + eventProcessor.onEvent("A1"); + eventProcessor.onEvent("A2"); + eventProcessor.onEvent("A3"); + + tb.join(); + } + + @Test + @Ignore // The test is too long for continuous testing. + // 2 threads, 3 events each. + public void test3_3() throws Exception { + boolean sawTheValidSequence = false; + + for (; ; ) { + eventProcessor.startIteration(); + run3_3_TestAction(); + final boolean needMoreIterations = eventProcessor.finishIteration(); + + sawTheValidSequence = sawTheValidSequence || + SOME_VALID_SEQUENCE_3_3.equals(eventProcessor.getCurrentSequenceString()); + + if (!needMoreIterations) break; + } + + assertEquals("Wrong number of leaf nodes", + factorial(3 + 3) / (factorial(3) * factorial(3)), + eventProcessor.numberOfLeafNodes()); + 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 { + boolean sawTheValidSequence = false; + + for (; ; ) { + eventProcessor.startIteration(); + Thread tb = new Thread(() -> { + eventProcessor.onEvent("B1:enter"); + eventProcessor.onEvent("B1:exit"); + eventProcessor.onEvent("B2"); + eventProcessor.onEvent("B3:enter"); + eventProcessor.onEvent("B3:exit"); + }); + tb.start(); + + eventProcessor.onEvent("A1"); + eventProcessor.onEvent("A2:enter"); + eventProcessor.onEvent("A2:exit"); + eventProcessor.onEvent("A3:enter"); + eventProcessor.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 { + eventProcessor = new RaceConditionReproducer(SOME_VALID_SEQUENCE_3_3); + eventProcessor.startIteration(); + run3_3_TestAction(); + assertTrue(!eventProcessor.finishIteration()); + assertEquals(SOME_VALID_SEQUENCE_3_3, eventProcessor.getCurrentSequenceString()); + + assertEquals("Wrong number of leaf nodes", 1, eventProcessor.numberOfLeafNodes()); + } + + @Test + @Ignore // The test is too long for continuous testing. + // 2 threads with 2 events; 1 thread with 1 event. + public void test2_1_2() throws Exception { + for (; ; ) { + eventProcessor.startIteration(); + Thread tb = new Thread(() -> { + eventProcessor.onEvent("B1"); + eventProcessor.onEvent("B2"); + }); + tb.start(); + + Thread tc = new Thread(() -> { + eventProcessor.onEvent("C1"); + }); + tc.start(); + + eventProcessor.onEvent("A1"); + eventProcessor.onEvent("A2"); + + 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()); + } + + @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 { + for (; ; ) { + eventProcessor.startIteration(); + Thread tb = new Thread(() -> { + eventProcessor.onEvent("B1:enter"); + eventProcessor.onEvent("B1:exit"); + eventProcessor.onEvent("B2:enter"); + eventProcessor.onEvent("B2:exit"); + }); + tb.start(); + + Thread tc = new Thread(() -> { + eventProcessor.onEvent("C1:enter"); + eventProcessor.onEvent("C1:exit"); + }); + tc.start(); + + eventProcessor.onEvent("A1:enter"); + eventProcessor.onEvent("A1:exit"); + eventProcessor.onEvent("A2:enter"); + eventProcessor.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()); + } +} diff --git a/tests/src/com/android/launcher3/util/TraceHelperForTest.java b/tests/src/com/android/launcher3/util/TraceHelperForTest.java new file mode 100644 index 0000000000..f1c8a67f9a --- /dev/null +++ b/tests/src/com/android/launcher3/util/TraceHelperForTest.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.util; + +import java.util.LinkedList; +import java.util.function.IntConsumer; + +public class TraceHelperForTest extends TraceHelper { + + private static final TraceHelperForTest INSTANCE_FOR_TEST = new TraceHelperForTest(); + + private final ThreadLocal> mStack = + ThreadLocal.withInitial(LinkedList::new); + + private RaceConditionReproducer mRaceConditionReproducer; + private IntConsumer mFlagsChangeListener; + + public static void setRaceConditionReproducer(RaceConditionReproducer reproducer) { + TraceHelper.INSTANCE = INSTANCE_FOR_TEST; + INSTANCE_FOR_TEST.mRaceConditionReproducer = reproducer; + } + + public static void cleanup() { + INSTANCE_FOR_TEST.mRaceConditionReproducer = null; + INSTANCE_FOR_TEST.mFlagsChangeListener = null; + } + + public static void setFlagsChangeListener(IntConsumer listener) { + TraceHelper.INSTANCE = INSTANCE_FOR_TEST; + INSTANCE_FOR_TEST.mFlagsChangeListener = listener; + } + + private TraceHelperForTest() { } + + @Override + public Object beginSection(String sectionName, int flags) { + LinkedList stack = mStack.get(); + TraceInfo info = new TraceInfo(sectionName, flags); + stack.add(info); + + if ((flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0 + && mRaceConditionReproducer != null) { + mRaceConditionReproducer.onEvent(RaceConditionReproducer.enterEvt(sectionName)); + } + updateBinderTracking(stack); + + super.beginSection(sectionName, flags); + return info; + } + + @Override + public void endSection(Object token) { + LinkedList stack = mStack.get(); + if (stack.size() == 0) { + new Throwable().printStackTrace(); + } + TraceInfo info = (TraceInfo) token; + stack.remove(info); + if ((info.flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0 + && mRaceConditionReproducer != null) { + mRaceConditionReproducer.onEvent(RaceConditionReproducer.exitEvt(info.sectionName)); + } + updateBinderTracking(stack); + + super.endSection(token); + } + + @Override + public Object beginFlagsOverride(int flags) { + LinkedList stack = mStack.get(); + TraceInfo info = new TraceInfo(null, flags); + stack.add(info); + updateBinderTracking(stack); + super.beginFlagsOverride(flags); + return info; + } + + @Override + public void endFlagsOverride(Object token) { + super.endFlagsOverride(token); + LinkedList stack = mStack.get(); + TraceInfo info = (TraceInfo) token; + stack.remove(info); + updateBinderTracking(stack); + } + + private void updateBinderTracking(LinkedList stack) { + if (mFlagsChangeListener != null) { + mFlagsChangeListener.accept(stack.stream() + .mapToInt(info -> info.flags).reduce(0, (a, b) -> a | b)); + } + } + + private static class TraceInfo { + public final String sectionName; + public final int flags; + + TraceInfo(String sectionName, int flags) { + this.sectionName = sectionName; + this.flags = flags; + } + } +}