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