blob: 9a94a4f54b615c76eb6be91d98edc5865030eb67 [file] [log] [blame]
/*
* Copyright (C) 2020 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.Trace.TRACE_TAG_WINDOW_MANAGER;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE;
import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION;
import android.annotation.NonNull;
import android.os.Trace;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.view.SurfaceControl;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import java.util.ArrayList;
/**
* Utility class for collecting WindowContainers that will merge transactions.
* For example to use to synchronously resize all the children of a window container
* 1. Open a new sync set, and pass the listener that will be invoked
* int id startSyncSet(TransactionReadyListener)
* the returned ID will be eventually passed to the TransactionReadyListener in combination
* with a set of WindowContainers that are ready, meaning onTransactionReady was called for
* those WindowContainers. You also use it to refer to the operation in future steps.
* 2. Ask each child to participate:
* addToSyncSet(int id, WindowContainer wc)
* if the child thinks it will be affected by a configuration change (a.k.a. has a visible
* window in its sub hierarchy, then we will increment a counter of expected callbacks
* At this point the containers hierarchy will redirect pendingTransaction and sub hierarchy
* updates in to the sync engine.
* 3. Apply your configuration changes to the window containers.
* 4. Tell the engine that the sync set is ready
* setReady(int id)
* 5. If there were no sub windows anywhere in the hierarchy to wait on, then
* transactionReady is immediately invoked, otherwise all the windows are poked
* to redraw and to deliver a buffer to {@link WindowState#finishDrawing}.
* Once all this drawing is complete, all the transactions will be merged and delivered
* to TransactionReadyListener.
*
* This works primarily by setting-up state and then watching/waiting for the registered subtrees
* to enter into a "finished" state (either by receiving drawn content or by disappearing). This
* checks the subtrees during surface-placement.
*/
class BLASTSyncEngine {
private static final String TAG = "BLASTSyncEngine";
interface TransactionReadyListener {
void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction);
}
/**
* Represents the desire to make a {@link BLASTSyncEngine.SyncGroup} while another is active.
*
* @see #queueSyncSet
*/
private static class PendingSyncSet {
/** Called immediately when the {@link BLASTSyncEngine} is free. */
private Runnable mStartSync;
/** Posted to the main handler after {@link #mStartSync} is called. */
private Runnable mApplySync;
}
/**
* Holds state associated with a single synchronous set of operations.
*/
class SyncGroup {
final int mSyncId;
final TransactionReadyListener mListener;
final Runnable mOnTimeout;
boolean mReady = false;
final ArraySet<WindowContainer> mRootMembers = new ArraySet<>();
private SurfaceControl.Transaction mOrphanTransaction = null;
private String mTraceName;
private SyncGroup(TransactionReadyListener listener, int id, String name) {
mSyncId = id;
mListener = listener;
mOnTimeout = () -> {
Slog.w(TAG, "Sync group " + mSyncId + " timeout");
synchronized (mWm.mGlobalLock) {
onTimeout();
}
};
if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
mTraceName = name + "SyncGroupReady";
Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, mTraceName, id);
}
}
/**
* Gets a transaction to dump orphaned operations into. Orphaned operations are operations
* that were on the mSyncTransactions of "root" subtrees which have been removed during the
* sync period.
*/
@NonNull
SurfaceControl.Transaction getOrphanTransaction() {
if (mOrphanTransaction == null) {
// Lazy since this isn't common
mOrphanTransaction = mWm.mTransactionFactory.get();
}
return mOrphanTransaction;
}
private void onSurfacePlacement() {
if (!mReady) return;
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: onSurfacePlacement checking %s",
mSyncId, mRootMembers);
for (int i = mRootMembers.size() - 1; i >= 0; --i) {
final WindowContainer wc = mRootMembers.valueAt(i);
if (!wc.isSyncFinished()) {
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Unfinished container: %s",
mSyncId, wc);
return;
}
}
finishNow();
}
private void finishNow() {
if (mTraceName != null) {
Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, mTraceName, mSyncId);
}
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Finished!", mSyncId);
SurfaceControl.Transaction merged = mWm.mTransactionFactory.get();
if (mOrphanTransaction != null) {
merged.merge(mOrphanTransaction);
}
for (WindowContainer wc : mRootMembers) {
wc.finishSync(merged, false /* cancel */);
}
final ArraySet<WindowContainer> wcAwaitingCommit = new ArraySet<>();
for (WindowContainer wc : mRootMembers) {
wc.waitForSyncTransactionCommit(wcAwaitingCommit);
}
class CommitCallback implements Runnable {
// Can run a second time if the action completes after the timeout.
boolean ran = false;
public void onCommitted() {
synchronized (mWm.mGlobalLock) {
if (ran) {
return;
}
mWm.mH.removeCallbacks(this);
ran = true;
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
for (WindowContainer wc : wcAwaitingCommit) {
wc.onSyncTransactionCommitted(t);
}
t.apply();
wcAwaitingCommit.clear();
}
}
// Called in timeout
@Override
public void run() {
// Sometimes we get a trace, sometimes we get a bugreport without
// a trace. Since these kind of ANRs can trigger such an issue,
// try and ensure we will have some visibility in both cases.
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionCommitTimeout");
Slog.e(TAG, "WM sent Transaction to organized, but never received" +
" commit callback. Application ANR likely to follow.");
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
onCommitted();
}
};
CommitCallback callback = new CommitCallback();
merged.addTransactionCommittedListener((r) -> { r.run(); }, callback::onCommitted);
mWm.mH.postDelayed(callback, BLAST_TIMEOUT_DURATION);
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionReady");
mListener.onTransactionReady(mSyncId, merged);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
mActiveSyncs.remove(mSyncId);
mWm.mH.removeCallbacks(mOnTimeout);
// Immediately start the next pending sync-transaction if there is one.
if (mActiveSyncs.size() == 0 && !mPendingSyncSets.isEmpty()) {
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "PendingStartTransaction found");
final PendingSyncSet pt = mPendingSyncSets.remove(0);
pt.mStartSync.run();
if (mActiveSyncs.size() == 0) {
throw new IllegalStateException("Pending Sync Set didn't start a sync.");
}
// Post this so that the now-playing transition setup isn't interrupted.
mWm.mH.post(() -> {
synchronized (mWm.mGlobalLock) {
pt.mApplySync.run();
}
});
}
}
private void setReady(boolean ready) {
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Set ready", mSyncId);
mReady = ready;
if (!ready) return;
mWm.mWindowPlacerLocked.requestTraversal();
}
private void addToSync(WindowContainer wc) {
if (!mRootMembers.add(wc)) {
return;
}
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Adding to group: %s", mSyncId, wc);
wc.setSyncGroup(this);
wc.prepareSync();
mWm.mWindowPlacerLocked.requestTraversal();
}
void onCancelSync(WindowContainer wc) {
mRootMembers.remove(wc);
}
private void onTimeout() {
if (!mActiveSyncs.contains(mSyncId)) return;
boolean allFinished = true;
for (int i = mRootMembers.size() - 1; i >= 0; --i) {
final WindowContainer<?> wc = mRootMembers.valueAt(i);
if (!wc.isSyncFinished()) {
allFinished = false;
Slog.i(TAG, "Unfinished container: " + wc);
}
}
if (allFinished && !mReady) {
Slog.w(TAG, "Sync group " + mSyncId + " timed-out because not ready. If you see "
+ "this, please file a bug.");
}
finishNow();
}
}
private final WindowManagerService mWm;
private int mNextSyncId = 0;
private final SparseArray<SyncGroup> mActiveSyncs = new SparseArray<>();
/**
* A queue of pending sync-sets waiting for their turn to run.
*
* @see #queueSyncSet
*/
private final ArrayList<PendingSyncSet> mPendingSyncSets = new ArrayList<>();
BLASTSyncEngine(WindowManagerService wms) {
mWm = wms;
}
/**
* Prepares a {@link SyncGroup} that is not active yet. Caller must call {@link #startSyncSet}
* before calling {@link #addToSyncSet(int, WindowContainer)} on any {@link WindowContainer}.
*/
SyncGroup prepareSyncSet(TransactionReadyListener listener, String name) {
return new SyncGroup(listener, mNextSyncId++, name);
}
int startSyncSet(TransactionReadyListener listener) {
return startSyncSet(listener, BLAST_TIMEOUT_DURATION, "");
}
int startSyncSet(TransactionReadyListener listener, long timeoutMs, String name) {
final SyncGroup s = prepareSyncSet(listener, name);
startSyncSet(s, timeoutMs);
return s.mSyncId;
}
void startSyncSet(SyncGroup s) {
startSyncSet(s, BLAST_TIMEOUT_DURATION);
}
void startSyncSet(SyncGroup s, long timeoutMs) {
if (mActiveSyncs.size() != 0) {
// We currently only support one sync at a time, so start a new SyncGroup when there is
// another may cause issue.
ProtoLog.w(WM_DEBUG_SYNC_ENGINE,
"SyncGroup %d: Started when there is other active SyncGroup", s.mSyncId);
}
mActiveSyncs.put(s.mSyncId, s);
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Started for listener: %s",
s.mSyncId, s.mListener);
scheduleTimeout(s, timeoutMs);
}
boolean hasActiveSync() {
return mActiveSyncs.size() != 0;
}
@VisibleForTesting
void scheduleTimeout(SyncGroup s, long timeoutMs) {
mWm.mH.postDelayed(s.mOnTimeout, timeoutMs);
}
void addToSyncSet(int id, WindowContainer wc) {
getSyncGroup(id).addToSync(wc);
}
void setReady(int id, boolean ready) {
getSyncGroup(id).setReady(ready);
}
void setReady(int id) {
setReady(id, true);
}
boolean isReady(int id) {
return getSyncGroup(id).mReady;
}
/**
* Aborts the sync (ie. it doesn't wait for ready or anything to finish)
*/
void abort(int id) {
getSyncGroup(id).finishNow();
}
private SyncGroup getSyncGroup(int id) {
final SyncGroup syncGroup = mActiveSyncs.get(id);
if (syncGroup == null) {
throw new IllegalStateException("SyncGroup is not started yet id=" + id);
}
return syncGroup;
}
void onSurfacePlacement() {
// backwards since each state can remove itself if finished
for (int i = mActiveSyncs.size() - 1; i >= 0; --i) {
mActiveSyncs.valueAt(i).onSurfacePlacement();
}
}
/**
* Queues a sync operation onto this engine. It will wait until any current/prior sync-sets
* have finished to run. This is needed right now because currently {@link BLASTSyncEngine}
* only supports 1 sync at a time.
*
* Code-paths should avoid using this unless absolutely necessary. Usually, we use this for
* difficult edge-cases that we hope to clean-up later.
*
* @param startSync will be called immediately when the {@link BLASTSyncEngine} is free to
* "reserve" the {@link BLASTSyncEngine} by calling one of the
* {@link BLASTSyncEngine#startSyncSet} variants.
* @param applySync will be posted to the main handler after {@code startSync} has been
* called. This is posted so that it doesn't interrupt any clean-up for the
* prior sync-set.
*/
void queueSyncSet(@NonNull Runnable startSync, @NonNull Runnable applySync) {
final PendingSyncSet pt = new PendingSyncSet();
pt.mStartSync = startSync;
pt.mApplySync = applySync;
mPendingSyncSets.add(pt);
}
/** @return {@code true} if there are any sync-sets waiting to start. */
boolean hasPendingSyncSets() {
return !mPendingSyncSets.isEmpty();
}
}