diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index f246cd9fa107b..acdff4fb52fd7 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -542,6 +542,21 @@ interface IWindowManager */ boolean isWindowTraceEnabled(); + /** + * Starts a transition trace. + */ + void startTransitionTrace(); + + /** + * Stops a transition trace. + */ + void stopTransitionTrace(); + + /** + * Returns true if transition trace is enabled. + */ + boolean isTransitionTraceEnabled(); + /** * Gets the windowing mode of the display. * diff --git a/core/proto/android/server/windowmanagertransitiontrace.proto b/core/proto/android/server/windowmanagertransitiontrace.proto new file mode 100644 index 0000000000000..9429127b2f6e6 --- /dev/null +++ b/core/proto/android/server/windowmanagertransitiontrace.proto @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 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. + */ + +syntax = "proto3"; + +package com.android.server.wm.shell; + +import "frameworks/base/core/proto/android/server/windowmanagerservice.proto"; + +option java_multiple_files = true; + +/* Represents a file full of transition entries. + Encoded, it should start with 0x9 0x57 0x49 0x4e 0x54 0x52 0x41 0x43 0x45 (.TRNTRACE), such + that it can be easily identified. */ +message TransitionTraceProto { + + /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L + (this is needed because enums have to be 32 bits and there's no nice way to put 64bit + constants into .proto files. */ + enum MagicNumber { + INVALID = 0; + MAGIC_NUMBER_L = 0x544e5254; /* TRNT (little-endian ASCII) */ + MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */ + } + + fixed64 magic_number = 1; /* Must be the first field, set to value in MagicNumber */ + int64 timestamp = 2; /* The timestamp of when the trace was started. */ + repeated Transition transition = 3; +} + +message Transition { + + enum State { + COLLECTING = 0; + PENDING = -1; + STARTED = 1; + PLAYING = 2; + ABORT = 3; + FINISHED = 4; + } + + int32 id = 1; + int32 transition_type = 2; + int64 timestamp = 3; + State state = 5; + int32 flags = 6; + repeated ChangeInfo change = 7; +} + +message ChangeInfo { + com.android.server.wm.IdentifierProto window_identifier = 1; + int32 transit_mode = 2; + bool has_changed = 3; + int32 change_flags = 4; +} diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 47eb715684119..84f6104652e33 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -95,6 +95,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.function.Predicate; /** @@ -131,18 +132,27 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe */ private static final int STATE_ABORT = 3; + /** + * This transition has finished playing successfully. + */ + private static final int STATE_FINISHED = 4; + @IntDef(prefix = { "STATE_" }, value = { STATE_PENDING, STATE_COLLECTING, STATE_STARTED, STATE_PLAYING, - STATE_ABORT + STATE_ABORT, + STATE_FINISHED }) @Retention(RetentionPolicy.SOURCE) @interface TransitionState {} final @TransitionType int mType; private int mSyncId = -1; + // Used for tracking a Transition throughout a lifecycle (i.e. from STATE_COLLECTING to + // STATE_FINISHED or STATE_ABORT), and should only be used for testing and debugging. + private int mDebugId = -1; private @TransitionFlags int mFlags; private final TransitionController mController; private final BLASTSyncEngine mSyncEngine; @@ -202,6 +212,8 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe mFlags = flags; mController = controller; mSyncEngine = syncEngine; + + controller.mTransitionTracer.logState(this); } void addFlag(int flag) { @@ -272,11 +284,21 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } } + @TransitionState + int getState() { + return mState; + } + @VisibleForTesting int getSyncId() { return mSyncId; } + @VisibleForTesting + int getDebugId() { + return mDebugId; + } + @TransitionFlags int getFlags() { return mFlags; @@ -289,6 +311,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } mState = STATE_COLLECTING; mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG); + mDebugId = mSyncId; + + mController.mTransitionTracer.logState(this); } /** @@ -306,6 +331,8 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Starting Transition %d", mSyncId); applyReady(); + + mController.mTransitionTracer.logState(this); } /** @@ -674,6 +701,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe dc.removeImeSurfaceImmediately(); dc.handleCompleteDeferredRemoval(); } + + mState = STATE_FINISHED; + mController.mTransitionTracer.logState(this); } void abort() { diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 15ab3a2803acb..6d31e937984ef 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -75,6 +75,7 @@ class TransitionController { private ITransitionPlayer mTransitionPlayer; final TransitionMetricsReporter mTransitionMetricsReporter = new TransitionMetricsReporter(); + final TransitionTracer mTransitionTracer; private IApplicationThread mTransitionPlayerThread; final ActivityTaskManagerService mAtm; @@ -100,10 +101,12 @@ class TransitionController { final StatusBarManagerInternal mStatusBar; TransitionController(ActivityTaskManagerService atm, - TaskSnapshotController taskSnapshotController) { + TaskSnapshotController taskSnapshotController, + TransitionTracer transitionTracer) { mAtm = atm; mStatusBar = LocalServices.getService(StatusBarManagerInternal.class); mTaskSnapshotController = taskSnapshotController; + mTransitionTracer = transitionTracer; mTransitionPlayerDeath = () -> { synchronized (mAtm.mGlobalLock) { // Clean-up/finish any playing transitions. @@ -524,6 +527,7 @@ class TransitionController { setAnimationRunning(true /* running */); } mPlayingTransitions.add(transition); + mTransitionTracer.logState(transition); } private void setAnimationRunning(boolean running) { @@ -542,6 +546,7 @@ class TransitionController { } transition.abort(); mCollectingTransition = null; + mTransitionTracer.logState(transition); } /** diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java new file mode 100644 index 0000000000000..192b9abc62a7b --- /dev/null +++ b/services/core/java/com/android/server/wm/TransitionTracer.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2022 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.server.wm; + +import static android.os.Build.IS_USER; + +import static com.android.server.wm.shell.ChangeInfo.CHANGE_FLAGS; +import static com.android.server.wm.shell.ChangeInfo.HAS_CHANGED; +import static com.android.server.wm.shell.ChangeInfo.TRANSIT_MODE; +import static com.android.server.wm.shell.ChangeInfo.WINDOW_IDENTIFIER; +import static com.android.server.wm.shell.Transition.CHANGE; +import static com.android.server.wm.shell.Transition.FLAGS; +import static com.android.server.wm.shell.Transition.ID; +import static com.android.server.wm.shell.Transition.STATE; +import static com.android.server.wm.shell.Transition.TIMESTAMP; +import static com.android.server.wm.shell.Transition.TRANSITION_TYPE; +import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER; +import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H; +import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L; +import static com.android.server.wm.shell.TransitionTraceProto.TRANSITION; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.SystemClock; +import android.os.Trace; +import android.util.Log; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.TraceBuffer; +import com.android.server.wm.Transition.ChangeInfo; +import com.android.server.wm.shell.TransitionTraceProto; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Helper class to collect and dump transition traces. + */ +public class TransitionTracer { + + private static final String LOG_TAG = "TransitionTracer"; + + /** + * Maximum buffer size, currently defined as 5 MB + */ + private static final int BUFFER_CAPACITY = 5120 * 1024; // 5 MB + static final String WINSCOPE_EXT = ".winscope"; + private static final String TRACE_FILE = "/data/misc/wmtrace/transition_trace" + WINSCOPE_EXT; + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + private final TransitionTraceBuffer mTraceBuffer = new TransitionTraceBuffer(); + + private final Object mEnabledLock = new Object(); + private volatile boolean mEnabled = false; + + private long mTraceStartTimestamp; + + private class TransitionTraceBuffer { + private final TraceBuffer mBuffer = new TraceBuffer(BUFFER_CAPACITY); + + private void pushTransitionState(Transition transition) { + final ProtoOutputStream outputStream = new ProtoOutputStream(); + final long transitionEntryToken = outputStream.start(TRANSITION); + + outputStream.write(ID, transition.getDebugId()); + outputStream.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos()); + outputStream.write(TRANSITION_TYPE, transition.mType); + outputStream.write(STATE, transition.getState()); + outputStream.write(FLAGS, transition.getFlags()); + + for (int i = 0; i < transition.mChanges.size(); ++i) { + final WindowContainer window = transition.mChanges.keyAt(i); + final ChangeInfo changeInfo = transition.mChanges.valueAt(i); + writeChange(outputStream, window, changeInfo); + } + + outputStream.end(transitionEntryToken); + + mBuffer.add(outputStream); + } + + private void writeChange(ProtoOutputStream outputStream, WindowContainer window, + ChangeInfo changeInfo) { + Trace.beginSection("TransitionProto#addChange"); + final long changeEntryToken = outputStream.start(CHANGE); + + final int transitMode = changeInfo.getTransitMode(window); + final boolean hasChanged = changeInfo.hasChanged(window); + final int changeFlags = changeInfo.getChangeFlags(window); + + outputStream.write(TRANSIT_MODE, transitMode); + outputStream.write(HAS_CHANGED, hasChanged); + outputStream.write(CHANGE_FLAGS, changeFlags); + window.writeIdentifierToProto(outputStream, WINDOW_IDENTIFIER); + + outputStream.end(changeEntryToken); + Trace.endSection(); + } + + public void writeToFile(File file, ProtoOutputStream proto) throws IOException { + mBuffer.writeTraceToFile(file, proto); + } + + public void reset() { + mBuffer.resetBuffer(); + } + } + + /** + * Records the current state of a transition in the transition trace (if it is running). + * @param transition the transition that we want to record the state of. + */ + public void logState(com.android.server.wm.Transition transition) { + if (!mEnabled) { + return; + } + + Log.d(LOG_TAG, "Logging state of transition " + transition); + mTraceBuffer.pushTransitionState(transition); + } + + /** + * Starts collecting transitions for the trace. + * If called while a trace is already running, this will reset the trace. + */ + public void startTrace(@Nullable PrintWriter pw) { + if (IS_USER) { + LogAndPrintln.e(pw, "Tracing is not supported on user builds."); + return; + } + Trace.beginSection("TransitionTracer#startTrace"); + LogAndPrintln.i(pw, "Starting shell transition trace."); + synchronized (mEnabledLock) { + mTraceStartTimestamp = SystemClock.elapsedRealtime(); + mEnabled = true; + mTraceBuffer.reset(); + } + Trace.endSection(); + } + + /** + * Stops collecting the transition trace and dump to trace to file. + * + * Dumps the trace to @link{TRACE_FILE}. + */ + public void stopTrace(@Nullable PrintWriter pw) { + stopTrace(pw, new File(TRACE_FILE)); + } + + /** + * Stops collecting the transition trace and dump to trace to file. + * @param outputFile The file to dump the transition trace to. + */ + public void stopTrace(@Nullable PrintWriter pw, File outputFile) { + if (IS_USER) { + LogAndPrintln.e(pw, "Tracing is not supported on user builds."); + return; + } + Trace.beginSection("TransitionTracer#stopTrace"); + LogAndPrintln.i(pw, "Stopping shell transition trace."); + synchronized (mEnabledLock) { + if (!mEnabled) { + LogAndPrintln.e(pw, + "Error: Tracing can't be stopped because it hasn't been started."); + return; + } + + mEnabled = false; + writeTraceToFileLocked(pw, outputFile); + } + Trace.endSection(); + } + + boolean isEnabled() { + return mEnabled; + } + + private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) { + Trace.beginSection("TransitionTracer#writeTraceToFileLocked"); + try { + ProtoOutputStream proto = new ProtoOutputStream(); + proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); + proto.write(TransitionTraceProto.TIMESTAMP, mTraceStartTimestamp); + int pid = android.os.Process.myPid(); + LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath() + + " from process " + pid); + mTraceBuffer.writeToFile(file, proto); + } catch (IOException e) { + LogAndPrintln.e(pw, "Unable to write buffer to file", e); + } + Trace.endSection(); + } + + private static class LogAndPrintln { + private static void i(@Nullable PrintWriter pw, String msg) { + Log.i(LOG_TAG, msg); + if (pw != null) { + pw.println(msg); + pw.flush(); + } + } + + private static void e(@Nullable PrintWriter pw, String msg) { + Log.e(LOG_TAG, msg); + if (pw != null) { + pw.println("ERROR: " + msg); + pw.flush(); + } + } + + private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) { + Log.e(LOG_TAG, msg, e); + if (pw != null) { + pw.println("ERROR: " + msg + " ::\n " + e); + pw.flush(); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index bdd2a3b793682..a01902b90a71b 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -459,6 +459,7 @@ public class WindowManagerService extends IWindowManager.Stub final WindowManagerConstants mConstants; final WindowTracing mWindowTracing; + final TransitionTracer mTransitionTracer; private final DisplayAreaPolicy.Provider mDisplayAreaPolicyProvider; @@ -1238,6 +1239,7 @@ public class WindowManagerService extends IWindowManager.Stub mWindowTracing = WindowTracing.createDefaultAndStartLooper(this, Choreographer.getInstance()); + mTransitionTracer = new TransitionTracer(); LocalServices.addService(WindowManagerPolicy.class, mPolicy); @@ -5881,6 +5883,21 @@ public class WindowManagerService extends IWindowManager.Stub return mWindowTracing.isEnabled(); } + @Override + public void startTransitionTrace() { + mTransitionTracer.startTrace(null /* printwriter */); + } + + @Override + public void stopTransitionTrace() { + mTransitionTracer.stopTrace(null /* printwriter */); + } + + @Override + public boolean isTransitionTraceEnabled() { + return mTransitionTracer.isEnabled(); + } + @Override public boolean registerCrossWindowBlurEnabledListener( ICrossWindowBlurEnabledListener listener) { diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index d9d804cab4494..02f056cd33af0 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -149,6 +149,8 @@ public class WindowManagerShellCommand extends ShellCommand { return runReset(pw); case "disable-blur": return runSetBlurDisabled(pw); + case "shell": + return runWmShellCommand(pw); default: return handleDefaultCommands(cmd); } @@ -1235,6 +1237,47 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } + private int runWmShellCommand(PrintWriter pw) { + String arg = getNextArg(); + + switch (arg) { + case "tracing": + return runWmShellTracing(pw); + case "help": + default: + return runHelp(pw); + } + } + + private int runHelp(PrintWriter pw) { + pw.println("Window Manager Shell commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" tracing "); + pw.println(" Start/stop shell transition tracing."); + + return 0; + } + + private int runWmShellTracing(PrintWriter pw) { + String arg = getNextArg(); + + switch (arg) { + case "start": + mInternal.mTransitionTracer.startTrace(pw); + break; + case "stop": + mInternal.mTransitionTracer.stopTrace(pw); + break; + default: + getErrPrintWriter() + .println("Error: expected 'start' or 'stop', but got '" + arg + "'"); + return -1; + } + + return 0; + } + private int runReset(PrintWriter pw) throws RemoteException { int displayId = getDisplayId(getNextArg()); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index c383ffd6af077..d4d8dd8bceb1a 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -149,7 +149,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } void setWindowManager(WindowManagerService wms) { - mTransitionController = new TransitionController(mService, wms.mTaskSnapshotController); + mTransitionController = new TransitionController(mService, wms.mTaskSnapshotController, + wms.mTransitionTracer); mTransitionController.registerLegacyListener(wms.mActivityManagerAppTransitionNotifier); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index d9d819b5f00f4..234bfa7ad7727 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -88,7 +88,11 @@ public class TransitionTests extends WindowTestsBase { final SurfaceControl.Transaction mMockT = mock(SurfaceControl.Transaction.class); private Transition createTestTransition(int transitType) { - TransitionController controller = mock(TransitionController.class); + TransitionTracer tracer = mock(TransitionTracer.class); + final TransitionController controller = new TransitionController( + mock(ActivityTaskManagerService.class), mock(TaskSnapshotController.class), + mock(TransitionTracer.class)); + final BLASTSyncEngine sync = createTestBLASTSyncEngine(); final Transition t = new Transition(transitType, 0 /* flags */, controller, sync); t.startCollecting(0 /* timeoutMs */); @@ -584,7 +588,7 @@ public class TransitionTests extends WindowTestsBase { @Test public void testTimeout() { final TransitionController controller = new TransitionController(mAtm, - mock(TaskSnapshotController.class)); + mock(TaskSnapshotController.class), mock(TransitionTracer.class)); final BLASTSyncEngine sync = new BLASTSyncEngine(mWm); final CountDownLatch latch = new CountDownLatch(1); // When the timeout is reached, it will finish the sync-group and notify transaction ready. @@ -841,7 +845,8 @@ public class TransitionTests extends WindowTestsBase { @Test public void testIntermediateVisibility() { final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class); - final TransitionController controller = new TransitionController(mAtm, snapshotController); + final TransitionController controller = new TransitionController(mAtm, snapshotController, + mock(TransitionTracer.class)); final ITransitionPlayer player = new ITransitionPlayer.Default(); controller.registerTransitionPlayer(player, null /* appThread */); final Transition openTransition = controller.createTransition(TRANSIT_OPEN); @@ -905,7 +910,8 @@ public class TransitionTests extends WindowTestsBase { @Test public void testTransientLaunch() { final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class); - final TransitionController controller = new TransitionController(mAtm, snapshotController); + final TransitionController controller = new TransitionController(mAtm, snapshotController, + mock(TransitionTracer.class)); final ITransitionPlayer player = new ITransitionPlayer.Default(); controller.registerTransitionPlayer(player, null /* appThread */); final Transition openTransition = controller.createTransition(TRANSIT_OPEN); @@ -968,7 +974,8 @@ public class TransitionTests extends WindowTestsBase { @Test public void testNotReadyPushPop() { final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class); - final TransitionController controller = new TransitionController(mAtm, snapshotController); + final TransitionController controller = new TransitionController(mAtm, snapshotController, + mock(TransitionTracer.class)); final ITransitionPlayer player = new ITransitionPlayer.Default(); controller.registerTransitionPlayer(player, null /* appThread */); final Transition openTransition = controller.createTransition(TRANSIT_OPEN);