/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.debug;
import android.view.Choreographer;
import com.facebook.react.bridge.ReactBridge;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
import com.facebook.react.common.LongArray;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.debug.NotThreadSafeUiManagerDebugListener;
/**
* Debug object that listens to bridge busy/idle events and UiManagerModule dispatches and uses it
* to calculate whether JS was able to update the UI during a given frame. After being installed
* on a {@link ReactBridge} and a {@link UIManagerModule},
* {@link #getDidJSHitFrameAndCleanup} should be called once per frame via a
* {@link Choreographer.FrameCallback}.
*/
public class DidJSUpdateUiDuringFrameDetector implements NotThreadSafeBridgeIdleDebugListener,
NotThreadSafeUiManagerDebugListener {
private final LongArray mTransitionToIdleEvents = LongArray.createWithInitialCapacity(20);
private final LongArray mTransitionToBusyEvents = LongArray.createWithInitialCapacity(20);
private final LongArray mViewHierarchyUpdateEnqueuedEvents =
LongArray.createWithInitialCapacity(20);
private final LongArray mViewHierarchyUpdateFinishedEvents =
LongArray.createWithInitialCapacity(20);
private volatile boolean mWasIdleAtEndOfLastFrame = true;
@Override
public synchronized void onTransitionToBridgeIdle() {
mTransitionToIdleEvents.add(System.nanoTime());
}
@Override
public synchronized void onTransitionToBridgeBusy() {
mTransitionToBusyEvents.add(System.nanoTime());
}
@Override
public synchronized void onViewHierarchyUpdateEnqueued() {
mViewHierarchyUpdateEnqueuedEvents.add(System.nanoTime());
}
@Override
public synchronized void onViewHierarchyUpdateFinished() {
mViewHierarchyUpdateFinishedEvents.add(System.nanoTime());
}
/**
* Designed to be called from a {@link Choreographer.FrameCallback#doFrame} call.
*
* There are two 'success' cases that will cause {@link #getDidJSHitFrameAndCleanup} to
* return true for a given frame:
*
* 1) UIManagerModule finished dispatching a batched UI update on the UI thread during the frame.
* This means that during the next hierarchy traversal, new UI will be drawn if needed (good).
* 2) The bridge ended the frame idle (meaning there were no JS nor native module calls still in
* flight) AND there was no UiManagerModule update enqueued that didn't also finish. NB: if
* there was one enqueued that actually finished, we'd have case 1), so effectively we just
* look for whether one was enqueued.
*
* NB: This call can only be called once for a given frame time range because it cleans up
* events it recorded for that frame.
*
* NB2: This makes the assumption that onViewHierarchyUpdateEnqueued is called from the
* {@link UIManagerModule#onBatchComplete()}, e.g. while the bridge is still considered busy,
* which means there is no race condition where the bridge has gone idle but a hierarchy update is
* waiting to be enqueued.
*
* @param frameStartTimeNanos the time in nanos that the last frame started
* @param frameEndTimeNanos the time in nanos that the last frame ended
*/
public synchronized boolean getDidJSHitFrameAndCleanup(
long frameStartTimeNanos,
long frameEndTimeNanos) {
// Case 1: We dispatched a UI update
boolean finishedUiUpdate = hasEventBetweenTimestamps(
mViewHierarchyUpdateFinishedEvents,
frameStartTimeNanos,
frameEndTimeNanos);
boolean didEndFrameIdle = didEndFrameIdle(frameStartTimeNanos, frameEndTimeNanos);
boolean hitFrame;
if (finishedUiUpdate) {
hitFrame = true;
} else {
// Case 2: Ended idle but no UI was enqueued during that frame
hitFrame = didEndFrameIdle && !hasEventBetweenTimestamps(
mViewHierarchyUpdateEnqueuedEvents,
frameStartTimeNanos,
frameEndTimeNanos);
}
cleanUp(mTransitionToIdleEvents, frameEndTimeNanos);
cleanUp(mTransitionToBusyEvents, frameEndTimeNanos);
cleanUp(mViewHierarchyUpdateEnqueuedEvents, frameEndTimeNanos);
cleanUp(mViewHierarchyUpdateFinishedEvents, frameEndTimeNanos);
mWasIdleAtEndOfLastFrame = didEndFrameIdle;
return hitFrame;
}
private static boolean hasEventBetweenTimestamps(
LongArray eventArray,
long startTime,
long endTime) {
for (int i = 0; i < eventArray.size(); i++) {
long time = eventArray.get(i);
if (time >= startTime && time < endTime) {
return true;
}
}
return false;
}
private static long getLastEventBetweenTimestamps(
LongArray eventArray,
long startTime,
long endTime) {
long lastEvent = -1;
for (int i = 0; i < eventArray.size(); i++) {
long time = eventArray.get(i);
if (time >= startTime && time < endTime) {
lastEvent = time;
} else if (time >= endTime) {
break;
}
}
return lastEvent;
}
private boolean didEndFrameIdle(long startTime, long endTime) {
long lastIdleTransition = getLastEventBetweenTimestamps(
mTransitionToIdleEvents,
startTime,
endTime);
long lastBusyTransition = getLastEventBetweenTimestamps(
mTransitionToBusyEvents,
startTime,
endTime);
if (lastIdleTransition == -1 && lastBusyTransition == -1) {
return mWasIdleAtEndOfLastFrame;
}
return lastIdleTransition > lastBusyTransition;
}
private static void cleanUp(LongArray eventArray, long endTime) {
int size = eventArray.size();
int indicesToRemove = 0;
for (int i = 0; i < size; i++) {
if (eventArray.get(i) < endTime) {
indicesToRemove++;
}
}
if (indicesToRemove > 0) {
for (int i = 0; i < size - indicesToRemove; i++) {
eventArray.set(i, eventArray.get(i + indicesToRemove));
}
eventArray.dropTail(indicesToRemove);
}
}
}