1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wm;
18 
19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
20 
21 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE;
22 import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.os.Handler;
27 import android.os.Trace;
28 import android.util.ArraySet;
29 import android.util.Slog;
30 import android.view.SurfaceControl;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.protolog.common.ProtoLog;
34 
35 import java.util.ArrayList;
36 
37 /**
38  * Utility class for collecting WindowContainers that will merge transactions.
39  * For example to use to synchronously resize all the children of a window container
40  *   1. Open a new sync set, and pass the listener that will be invoked
41  *        int id startSyncSet(TransactionReadyListener)
42  *      the returned ID will be eventually passed to the TransactionReadyListener in combination
43  *      with a set of WindowContainers that are ready, meaning onTransactionReady was called for
44  *      those WindowContainers. You also use it to refer to the operation in future steps.
45  *   2. Ask each child to participate:
46  *       addToSyncSet(int id, WindowContainer wc)
47  *      if the child thinks it will be affected by a configuration change (a.k.a. has a visible
48  *      window in its sub hierarchy, then we will increment a counter of expected callbacks
49  *      At this point the containers hierarchy will redirect pendingTransaction and sub hierarchy
50  *      updates in to the sync engine.
51  *   3. Apply your configuration changes to the window containers.
52  *   4. Tell the engine that the sync set is ready
53  *       setReady(int id)
54  *   5. If there were no sub windows anywhere in the hierarchy to wait on, then
55  *      transactionReady is immediately invoked, otherwise all the windows are poked
56  *      to redraw and to deliver a buffer to {@link WindowState#finishDrawing}.
57  *      Once all this drawing is complete, all the transactions will be merged and delivered
58  *      to TransactionReadyListener.
59  *
60  * This works primarily by setting-up state and then watching/waiting for the registered subtrees
61  * to enter into a "finished" state (either by receiving drawn content or by disappearing). This
62  * checks the subtrees during surface-placement.
63  *
64  * By default, all Syncs will be serialized (and it is an error to start one while another is
65  * active). However, a sync can be explicitly started in "parallel". This does not guarantee that
66  * it will run in parallel; however, it will run in parallel as long as it's watched hierarchy
67  * doesn't overlap with any other syncs' watched hierarchies.
68  *
69  * Currently, a sync that is started as "parallel" implicitly ignores the subtree below it's
70  * direct members unless those members are activities (WindowStates are considered "part of" the
71  * activity). This allows "stratified" parallelism where, eg, a sync that is only at Task-level
72  * can run in parallel with another sync that includes only the task's activities.
73  *
74  * If, at any time, a container is added to a parallel sync that *is* watched by another sync, it
75  * will be forced to serialize with it. This is done by adding a dependency. A sync will only
76  * finish if it has no active dependencies. At this point it is effectively not parallel anymore.
77  *
78  * To avoid dependency cycles, if a sync B ultimately depends on a sync A and a container is added
79  * to A which is watched by B, that container will, instead, be moved from B to A instead of
80  * creating a cyclic dependency.
81  *
82  * When syncs overlap, this will attempt to finish everything in the order they were started.
83  */
84 class BLASTSyncEngine {
85     private static final String TAG = "BLASTSyncEngine";
86 
87     /** No specific method. Used by override specifiers. */
88     public static final int METHOD_UNDEFINED = -1;
89 
90     /** No sync method. Apps will draw/present internally and just report. */
91     public static final int METHOD_NONE = 0;
92 
93     /** Sync with BLAST. Apps will draw and then send the buffer to be applied in sync. */
94     public static final int METHOD_BLAST = 1;
95 
96     interface TransactionReadyListener {
onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction)97         void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction);
onTransactionCommitTimeout()98         default void onTransactionCommitTimeout() {}
onReadyTimeout()99         default void onReadyTimeout() {}
100     }
101 
102     /**
103      * Represents the desire to make a {@link BLASTSyncEngine.SyncGroup} while another is active.
104      *
105      * @see #queueSyncSet
106      */
107     private static class PendingSyncSet {
108         /** Called immediately when the {@link BLASTSyncEngine} is free. */
109         private Runnable mStartSync;
110 
111         /** Posted to the main handler after {@link #mStartSync} is called. */
112         private Runnable mApplySync;
113     }
114 
115     /**
116      * Holds state associated with a single synchronous set of operations.
117      */
118     class SyncGroup {
119         final int mSyncId;
120         final String mSyncName;
121         int mSyncMethod = METHOD_BLAST;
122         final TransactionReadyListener mListener;
123         final Runnable mOnTimeout;
124         boolean mReady = false;
125         final ArraySet<WindowContainer> mRootMembers = new ArraySet<>();
126         private SurfaceControl.Transaction mOrphanTransaction = null;
127         private String mTraceName;
128 
129         private static final ArrayList<SyncGroup> NO_DEPENDENCIES = new ArrayList<>();
130 
131         /**
132          * When `true`, this SyncGroup will only wait for mRootMembers to draw; otherwise,
133          * it waits for the whole subtree(s) rooted at the mRootMembers.
134          */
135         boolean mIgnoreIndirectMembers = false;
136 
137         /** List of SyncGroups that must finish before this one can. */
138         @NonNull
139         ArrayList<SyncGroup> mDependencies = NO_DEPENDENCIES;
140 
SyncGroup(TransactionReadyListener listener, int id, String name)141         private SyncGroup(TransactionReadyListener listener, int id, String name) {
142             mSyncId = id;
143             mSyncName = name;
144             mListener = listener;
145             mOnTimeout = () -> {
146                 Slog.w(TAG, "Sync group " + mSyncId + " timeout");
147                 synchronized (mWm.mGlobalLock) {
148                     onTimeout();
149                 }
150             };
151             if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
152                 mTraceName = name + "SyncGroupReady";
153                 Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, mTraceName, id);
154             }
155         }
156 
157         /**
158          * Gets a transaction to dump orphaned operations into. Orphaned operations are operations
159          * that were on the mSyncTransactions of "root" subtrees which have been removed during the
160          * sync period.
161          */
162         @NonNull
getOrphanTransaction()163         SurfaceControl.Transaction getOrphanTransaction() {
164             if (mOrphanTransaction == null) {
165                 // Lazy since this isn't common
166                 mOrphanTransaction = mWm.mTransactionFactory.get();
167             }
168             return mOrphanTransaction;
169         }
170 
171         /**
172          * Check if the sync-group ignores a particular container. This is used to allow syncs at
173          * different levels to run in parallel. The primary example is Recents while an activity
174          * sync is happening.
175          */
isIgnoring(WindowContainer wc)176         boolean isIgnoring(WindowContainer wc) {
177             // Some heuristics to avoid unnecessary work:
178             // 1. For now, require an explicit acknowledgement of potential "parallelism" across
179             //    hierarchy levels (horizontal).
180             if (!mIgnoreIndirectMembers) return false;
181             // 2. Don't check WindowStates since they are below the relevant abstraction level (
182             //    anything activity/token and above).
183             if (wc.asWindowState() != null) return false;
184             // Obviously, don't ignore anything that is directly part of this group.
185             return wc.mSyncGroup != this;
186         }
187 
188         /** @return `true` if it finished. */
tryFinish()189         private boolean tryFinish() {
190             if (!mReady) return false;
191             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: onSurfacePlacement checking %s",
192                     mSyncId, mRootMembers);
193             if (!mDependencies.isEmpty()) {
194                 ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d:  Unfinished dependencies: %s",
195                         mSyncId, mDependencies);
196                 return false;
197             }
198             for (int i = mRootMembers.size() - 1; i >= 0; --i) {
199                 final WindowContainer wc = mRootMembers.valueAt(i);
200                 if (!wc.isSyncFinished(this)) {
201                     ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d:  Unfinished container: %s",
202                             mSyncId, wc);
203                     return false;
204                 }
205             }
206             finishNow();
207             return true;
208         }
209 
finishNow()210         private void finishNow() {
211             if (mTraceName != null) {
212                 Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, mTraceName, mSyncId);
213             }
214             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Finished!", mSyncId);
215             SurfaceControl.Transaction merged = mWm.mTransactionFactory.get();
216             if (mOrphanTransaction != null) {
217                 merged.merge(mOrphanTransaction);
218             }
219             for (WindowContainer wc : mRootMembers) {
220                 wc.finishSync(merged, this, false /* cancel */);
221             }
222 
223             final ArraySet<WindowContainer> wcAwaitingCommit = new ArraySet<>();
224             for (WindowContainer wc : mRootMembers) {
225                 wc.waitForSyncTransactionCommit(wcAwaitingCommit);
226             }
227 
228             final int syncId = mSyncId;
229             final long mergedTxId = merged.getId();
230             final String syncName = mSyncName;
231             class CommitCallback implements Runnable {
232                 // Can run a second time if the action completes after the timeout.
233                 boolean ran = false;
234                 public void onCommitted(SurfaceControl.Transaction t) {
235                     // Don't wait to hold the global lock to remove the timeout runnable
236                     mHandler.removeCallbacks(this);
237                     synchronized (mWm.mGlobalLock) {
238                         if (ran) {
239                             return;
240                         }
241                         ran = true;
242                         for (WindowContainer wc : wcAwaitingCommit) {
243                             wc.onSyncTransactionCommitted(t);
244                         }
245                         t.apply();
246                         wcAwaitingCommit.clear();
247                     }
248                 }
249 
250                 // Called in timeout
251                 @Override
252                 public void run() {
253                     // Sometimes we get a trace, sometimes we get a bugreport without
254                     // a trace. Since these kind of ANRs can trigger such an issue,
255                     // try and ensure we will have some visibility in both cases.
256                     Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionCommitTimeout");
257                     Slog.e(TAG, "WM sent Transaction (#" + syncId + ", " + syncName + ", tx="
258                             + mergedTxId + ") to organizer, but never received commit callback."
259                             + " Application ANR likely to follow.");
260                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
261                     synchronized (mWm.mGlobalLock) {
262                         mListener.onTransactionCommitTimeout();
263                         onCommitted(merged.mNativeObject != 0
264                                 ? merged : mWm.mTransactionFactory.get());
265                     }
266                 }
267             };
268             CommitCallback callback = new CommitCallback();
269             merged.addTransactionCommittedListener(Runnable::run,
270                     () -> callback.onCommitted(new SurfaceControl.Transaction()));
271             mHandler.postDelayed(callback, BLAST_TIMEOUT_DURATION);
272 
273             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionReady");
274             mListener.onTransactionReady(mSyncId, merged);
275             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
276             mActiveSyncs.remove(this);
277             mHandler.removeCallbacks(mOnTimeout);
278 
279             // Immediately start the next pending sync-transaction if there is one.
280             if (mActiveSyncs.size() == 0 && !mPendingSyncSets.isEmpty()) {
281                 ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "PendingStartTransaction found");
282                 final PendingSyncSet pt = mPendingSyncSets.remove(0);
283                 pt.mStartSync.run();
284                 if (mActiveSyncs.size() == 0) {
285                     throw new IllegalStateException("Pending Sync Set didn't start a sync.");
286                 }
287                 // Post this so that the now-playing transition setup isn't interrupted.
288                 mHandler.post(() -> {
289                     synchronized (mWm.mGlobalLock) {
290                         pt.mApplySync.run();
291                     }
292                 });
293             }
294             // Notify idle listeners
295             for (int i = mOnIdleListeners.size() - 1; i >= 0; --i) {
296                 // If an idle listener adds a sync, though, then stop notifying.
297                 if (mActiveSyncs.size() > 0) break;
298                 mOnIdleListeners.get(i).run();
299             }
300         }
301 
302         /** returns true if readiness changed. */
setReady(boolean ready)303         private boolean setReady(boolean ready) {
304             if (mReady == ready) {
305                 return false;
306             }
307             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Set ready %b", mSyncId, ready);
308             mReady = ready;
309             if (ready) {
310                 mWm.mWindowPlacerLocked.requestTraversal();
311             }
312             return true;
313         }
314 
addToSync(WindowContainer wc)315         private void addToSync(WindowContainer wc) {
316             if (mRootMembers.contains(wc)) {
317                 return;
318             }
319             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Adding to group: %s", mSyncId, wc);
320             final SyncGroup dependency = wc.getSyncGroup();
321             if (dependency != null && dependency != this && !dependency.isIgnoring(wc)) {
322                 // This syncgroup now conflicts with another one, so the whole group now must
323                 // wait on the other group.
324                 Slog.w(TAG, "SyncGroup " + mSyncId + " conflicts with " + dependency.mSyncId
325                         + ": Making " + mSyncId + " depend on " + dependency.mSyncId);
326                 if (mDependencies.contains(dependency)) {
327                     // nothing, it's already a dependency.
328                 } else if (dependency.dependsOn(this)) {
329                     Slog.w(TAG, " Detected dependency cycle between " + mSyncId + " and "
330                             + dependency.mSyncId + ": Moving " + wc + " to " + mSyncId);
331                     // Since dependency already depends on this, make this now `wc`'s watcher
332                     if (wc.mSyncGroup == null) {
333                         wc.setSyncGroup(this);
334                     } else {
335                         // Explicit replacement.
336                         wc.mSyncGroup.mRootMembers.remove(wc);
337                         mRootMembers.add(wc);
338                         wc.mSyncGroup = this;
339                     }
340                 } else {
341                     if (mDependencies == NO_DEPENDENCIES) {
342                         mDependencies = new ArrayList<>();
343                     }
344                     mDependencies.add(dependency);
345                 }
346             } else {
347                 mRootMembers.add(wc);
348                 wc.setSyncGroup(this);
349             }
350             wc.prepareSync();
351             if (mReady) {
352                 mWm.mWindowPlacerLocked.requestTraversal();
353             }
354         }
355 
dependsOn(SyncGroup group)356         private boolean dependsOn(SyncGroup group) {
357             if (mDependencies.isEmpty()) return false;
358             // BFS search with membership check. We don't expect cycle here (since this is
359             // explicitly called to avoid cycles) but just to be safe.
360             final ArrayList<SyncGroup> fringe = mTmpFringe;
361             fringe.clear();
362             fringe.add(this);
363             for (int head = 0; head < fringe.size(); ++head) {
364                 final SyncGroup next = fringe.get(head);
365                 if (next == group) {
366                     fringe.clear();
367                     return true;
368                 }
369                 for (int i = 0; i < next.mDependencies.size(); ++i) {
370                     if (fringe.contains(next.mDependencies.get(i))) continue;
371                     fringe.add(next.mDependencies.get(i));
372                 }
373             }
374             fringe.clear();
375             return false;
376         }
377 
onCancelSync(WindowContainer wc)378         void onCancelSync(WindowContainer wc) {
379             mRootMembers.remove(wc);
380         }
381 
onTimeout()382         private void onTimeout() {
383             if (!mActiveSyncs.contains(this)) return;
384             boolean allFinished = true;
385             for (int i = mRootMembers.size() - 1; i >= 0; --i) {
386                 final WindowContainer<?> wc = mRootMembers.valueAt(i);
387                 if (!wc.isSyncFinished(this)) {
388                     allFinished = false;
389                     Slog.i(TAG, "Unfinished container: " + wc);
390                     wc.forAllActivities(a -> {
391                         if (a.isVisibleRequested()) {
392                             if (a.isRelaunching()) {
393                                 Slog.i(TAG, "  " + a + " is relaunching");
394                             }
395                             a.forAllWindows(w -> {
396                                 Slog.i(TAG, "  " + w + " " + w.mWinAnimator.drawStateToString());
397                             }, true /* traverseTopToBottom */);
398                         } else if (a.mDisplayContent != null && !a.mDisplayContent
399                                 .mUnknownAppVisibilityController.allResolved()) {
400                             Slog.i(TAG, "  UnknownAppVisibility: " + a.mDisplayContent
401                                     .mUnknownAppVisibilityController.getDebugMessage());
402                         }
403                     });
404                 }
405             }
406 
407             for (int i = mDependencies.size() - 1; i >= 0; --i) {
408                 allFinished = false;
409                 Slog.i(TAG, "Unfinished dependency: " + mDependencies.get(i).mSyncId);
410             }
411             if (allFinished && !mReady) {
412                 Slog.w(TAG, "Sync group " + mSyncId + " timed-out because not ready. If you see "
413                         + "this, please file a bug.");
414                 mListener.onReadyTimeout();
415             }
416             finishNow();
417             removeFromDependencies(this);
418         }
419     }
420 
421     private final WindowManagerService mWm;
422     private final Handler mHandler;
423     private int mNextSyncId = 0;
424 
425     /** Currently active syncs. Intentionally ordered by start time. */
426     private final ArrayList<SyncGroup> mActiveSyncs = new ArrayList<>();
427 
428     /**
429      * A queue of pending sync-sets waiting for their turn to run.
430      *
431      * @see #queueSyncSet
432      */
433     private final ArrayList<PendingSyncSet> mPendingSyncSets = new ArrayList<>();
434 
435     private final ArrayList<Runnable> mOnIdleListeners = new ArrayList<>();
436 
437     private final ArrayList<SyncGroup> mTmpFinishQueue = new ArrayList<>();
438     private final ArrayList<SyncGroup> mTmpFringe = new ArrayList<>();
439 
BLASTSyncEngine(WindowManagerService wms)440     BLASTSyncEngine(WindowManagerService wms) {
441         this(wms, wms.mH);
442     }
443 
444     @VisibleForTesting
BLASTSyncEngine(WindowManagerService wms, Handler mainHandler)445     BLASTSyncEngine(WindowManagerService wms, Handler mainHandler) {
446         mWm = wms;
447         mHandler = mainHandler;
448     }
449 
450     /**
451      * Prepares a {@link SyncGroup} that is not active yet. Caller must call {@link #startSyncSet}
452      * before calling {@link #addToSyncSet(int, WindowContainer)} on any {@link WindowContainer}.
453      */
prepareSyncSet(TransactionReadyListener listener, String name)454     SyncGroup prepareSyncSet(TransactionReadyListener listener, String name) {
455         return new SyncGroup(listener, mNextSyncId++, name);
456     }
457 
startSyncSet(TransactionReadyListener listener, long timeoutMs, String name, boolean parallel)458     int startSyncSet(TransactionReadyListener listener, long timeoutMs, String name,
459             boolean parallel) {
460         final SyncGroup s = prepareSyncSet(listener, name);
461         startSyncSet(s, timeoutMs, parallel);
462         return s.mSyncId;
463     }
464 
startSyncSet(SyncGroup s)465     void startSyncSet(SyncGroup s) {
466         startSyncSet(s, BLAST_TIMEOUT_DURATION, false /* parallel */);
467     }
468 
startSyncSet(SyncGroup s, long timeoutMs, boolean parallel)469     void startSyncSet(SyncGroup s, long timeoutMs, boolean parallel) {
470         final boolean alreadyRunning = mActiveSyncs.size() > 0;
471         if (!parallel && alreadyRunning) {
472             // We only support overlapping syncs when explicitly declared `parallel`.
473             Slog.e(TAG, "SyncGroup " + s.mSyncId
474                     + ": Started when there is other active SyncGroup");
475         }
476         mActiveSyncs.add(s);
477         // For now, parallel implies this.
478         s.mIgnoreIndirectMembers = parallel;
479         ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Started %sfor listener: %s",
480                 s.mSyncId, (parallel && alreadyRunning ? "(in parallel) " : ""), s.mListener);
481         scheduleTimeout(s, timeoutMs);
482     }
483 
484     @Nullable
getSyncSet(int id)485     SyncGroup getSyncSet(int id) {
486         for (int i = 0; i < mActiveSyncs.size(); ++i) {
487             if (mActiveSyncs.get(i).mSyncId != id) continue;
488             return mActiveSyncs.get(i);
489         }
490         return null;
491     }
492 
hasActiveSync()493     boolean hasActiveSync() {
494         return mActiveSyncs.size() != 0;
495     }
496 
497     @VisibleForTesting
scheduleTimeout(SyncGroup s, long timeoutMs)498     void scheduleTimeout(SyncGroup s, long timeoutMs) {
499         mHandler.postDelayed(s.mOnTimeout, timeoutMs);
500     }
501 
addToSyncSet(int id, WindowContainer wc)502     void addToSyncSet(int id, WindowContainer wc) {
503         getSyncGroup(id).addToSync(wc);
504     }
505 
setSyncMethod(int id, int method)506     void setSyncMethod(int id, int method) {
507         final SyncGroup syncGroup = getSyncGroup(id);
508         if (!syncGroup.mRootMembers.isEmpty()) {
509             throw new IllegalStateException(
510                     "Not allow to change sync method after adding group member, id=" + id);
511         }
512         syncGroup.mSyncMethod = method;
513     }
514 
setReady(int id, boolean ready)515     boolean setReady(int id, boolean ready) {
516         return getSyncGroup(id).setReady(ready);
517     }
518 
setReady(int id)519     void setReady(int id) {
520         setReady(id, true);
521     }
522 
isReady(int id)523     boolean isReady(int id) {
524         return getSyncGroup(id).mReady;
525     }
526 
527     /**
528      * Aborts the sync (ie. it doesn't wait for ready or anything to finish)
529      */
abort(int id)530     void abort(int id) {
531         final SyncGroup group = getSyncGroup(id);
532         group.finishNow();
533         removeFromDependencies(group);
534     }
535 
getSyncGroup(int id)536     private SyncGroup getSyncGroup(int id) {
537         final SyncGroup syncGroup = getSyncSet(id);
538         if (syncGroup == null) {
539             throw new IllegalStateException("SyncGroup is not started yet id=" + id);
540         }
541         return syncGroup;
542     }
543 
544     /**
545      * Just removes `group` from any dependency lists. Does not try to evaluate anything. However,
546      * it will schedule traversals if any groups were changed in a way that could make them ready.
547      */
removeFromDependencies(SyncGroup group)548     private void removeFromDependencies(SyncGroup group) {
549         boolean anyChange = false;
550         for (int i = 0; i < mActiveSyncs.size(); ++i) {
551             final SyncGroup active = mActiveSyncs.get(i);
552             if (!active.mDependencies.remove(group)) continue;
553             if (!active.mDependencies.isEmpty()) continue;
554             anyChange = true;
555         }
556         if (!anyChange) return;
557         mWm.mWindowPlacerLocked.requestTraversal();
558     }
559 
onSurfacePlacement()560     void onSurfacePlacement() {
561         if (mActiveSyncs.isEmpty()) return;
562         // queue in-order since we want interdependent syncs to become ready in the same order they
563         // started in.
564         mTmpFinishQueue.addAll(mActiveSyncs);
565         // There shouldn't be any dependency cycles or duplicates, but add an upper-bound just
566         // in case. Assuming absolute worst case, each visit will try and revisit everything
567         // before it, so n + (n-1) + (n-2) ... = (n+1)*n/2
568         int visitBounds = ((mActiveSyncs.size() + 1) * mActiveSyncs.size()) / 2;
569         while (!mTmpFinishQueue.isEmpty()) {
570             if (visitBounds <= 0) {
571                 Slog.e(TAG, "Trying to finish more syncs than theoretically possible. This "
572                         + "should never happen. Most likely a dependency cycle wasn't detected.");
573             }
574             --visitBounds;
575             final SyncGroup group = mTmpFinishQueue.remove(0);
576             final int grpIdx = mActiveSyncs.indexOf(group);
577             // Skip if it's already finished:
578             if (grpIdx < 0) continue;
579             if (!group.tryFinish()) continue;
580             // Finished, so update dependencies of any prior groups and retry if unblocked.
581             int insertAt = 0;
582             for (int i = 0; i < mActiveSyncs.size(); ++i) {
583                 final SyncGroup active = mActiveSyncs.get(i);
584                 if (!active.mDependencies.remove(group)) continue;
585                 // Anything afterwards is already in queue.
586                 if (i >= grpIdx) continue;
587                 if (!active.mDependencies.isEmpty()) continue;
588                 // `active` became unblocked so it can finish, since it started earlier, it should
589                 // be checked next to maintain order.
590                 mTmpFinishQueue.add(insertAt, mActiveSyncs.get(i));
591                 insertAt += 1;
592             }
593         }
594     }
595 
596     /** Only use this for tests! */
tryFinishForTest(int syncId)597     void tryFinishForTest(int syncId) {
598         getSyncSet(syncId).tryFinish();
599     }
600 
601     /**
602      * Queues a sync operation onto this engine. It will wait until any current/prior sync-sets
603      * have finished to run. This is needed right now because currently {@link BLASTSyncEngine}
604      * only supports 1 sync at a time.
605      *
606      * Code-paths should avoid using this unless absolutely necessary. Usually, we use this for
607      * difficult edge-cases that we hope to clean-up later.
608      *
609      * @param startSync will be called immediately when the {@link BLASTSyncEngine} is free to
610      *                  "reserve" the {@link BLASTSyncEngine} by calling one of the
611      *                  {@link BLASTSyncEngine#startSyncSet} variants.
612      * @param applySync will be posted to the main handler after {@code startSync} has been
613      *                  called. This is posted so that it doesn't interrupt any clean-up for the
614      *                  prior sync-set.
615      */
queueSyncSet(@onNull Runnable startSync, @NonNull Runnable applySync)616     void queueSyncSet(@NonNull Runnable startSync, @NonNull Runnable applySync) {
617         final PendingSyncSet pt = new PendingSyncSet();
618         pt.mStartSync = startSync;
619         pt.mApplySync = applySync;
620         mPendingSyncSets.add(pt);
621     }
622 
623     /** @return {@code true} if there are any sync-sets waiting to start. */
hasPendingSyncSets()624     boolean hasPendingSyncSets() {
625         return !mPendingSyncSets.isEmpty();
626     }
627 
addOnIdleListener(Runnable onIdleListener)628     void addOnIdleListener(Runnable onIdleListener) {
629         mOnIdleListeners.add(onIdleListener);
630     }
631 }
632