diff --git a/litho-rendercore-sample/src/main/java/com/facebook/rendercore/sample/SampleActivity.kt b/litho-rendercore-sample/src/main/java/com/facebook/rendercore/sample/SampleActivity.kt index 7c79125b33b..c4efcde9be0 100644 --- a/litho-rendercore-sample/src/main/java/com/facebook/rendercore/sample/SampleActivity.kt +++ b/litho-rendercore-sample/src/main/java/com/facebook/rendercore/sample/SampleActivity.kt @@ -90,7 +90,7 @@ class SampleActivity : ComponentActivity(), RenderState.Delegate { currentRenderTree = next } - override fun commitToUI(tree: RenderTree?, state: SampleData?) { + override fun commitToUI(tree: RenderTree?, state: SampleData?, frameId: Int) { currentRenderTree = tree } } diff --git a/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RenderCoreTestRule.java b/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RenderCoreTestRule.java index 7d9a401a861..66ab40833f4 100644 --- a/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RenderCoreTestRule.java +++ b/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RenderCoreTestRule.java @@ -63,7 +63,7 @@ public void commit( Object nextState) {} @Override - public void commitToUI(RenderTree tree, Object o) {} + public void commitToUI(@Nullable RenderTree tree, @Nullable Object o, int frameVersion) {} }; private Context context; diff --git a/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RendercoreTestDriver.java b/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RendercoreTestDriver.java index cd2969aa91c..a60008e98a0 100644 --- a/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RendercoreTestDriver.java +++ b/litho-rendercore-testing/src/main/java/com/facebook/rendercore/testing/RendercoreTestDriver.java @@ -147,7 +147,8 @@ public void commit( Object nextState) {} @Override - public void commitToUI(RenderTree tree, Object o) {} + public void commitToUI( + @Nullable RenderTree tree, @Nullable Object o, int frameVersion) {} }, null, null); diff --git a/litho-rendercore/src/main/java/com/facebook/rendercore/LayoutFuture.kt b/litho-rendercore/src/main/java/com/facebook/rendercore/LayoutFuture.kt index 3cfce5e5eb0..8d28d478531 100644 --- a/litho-rendercore/src/main/java/com/facebook/rendercore/LayoutFuture.kt +++ b/litho-rendercore/src/main/java/com/facebook/rendercore/LayoutFuture.kt @@ -26,6 +26,7 @@ class LayoutFuture( val tree: Node, state: State?, val version: Int, + val frameId: Int, previousResult: RenderResult?, extensions: Array>?, val sizeConstraints: SizeConstraints diff --git a/litho-rendercore/src/main/java/com/facebook/rendercore/RenderState.kt b/litho-rendercore/src/main/java/com/facebook/rendercore/RenderState.kt index cbb18a04aa5..f0894e94508 100644 --- a/litho-rendercore/src/main/java/com/facebook/rendercore/RenderState.kt +++ b/litho-rendercore/src/main/java/com/facebook/rendercore/RenderState.kt @@ -24,10 +24,12 @@ import com.facebook.infer.annotation.ThreadConfined import com.facebook.rendercore.StateUpdateReceiver.StateUpdate import com.facebook.rendercore.extensions.RenderCoreExtension import com.facebook.rendercore.utils.ThreadUtils +import com.facebook.rendercore.utils.VSyncUtils import java.util.Objects import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicInteger import javax.annotation.concurrent.ThreadSafe +import kotlin.math.roundToInt /** todo: javadocs * */ class RenderState> @@ -75,7 +77,7 @@ constructor( nextState: State? ) - fun commitToUI(tree: RenderTree?, state: State?) + fun commitToUI(tree: RenderTree?, state: State?, frameVersion: Int) } fun interface HostListener { @@ -94,14 +96,20 @@ constructor( private var latestResolveFunc: ResolveFunc? = null private var resolveFuture: ResolveFuture? = null private var layoutFuture: LayoutFuture? = null + private var resolveVersionCounter = 0 + private var committedResolveVersion = UNSET private var committedResolvedTree: Node? = null private var committedState: State? = null + private var committedResolvedFrameId = UNSET private val pendingStateUpdates: MutableList = ArrayList() private var committedRenderResult: RenderResult? = null - private var resolveVersionCounter = 0 + + private val normalVSyncTime = VSyncUtils.getNormalVsyncTime(context) + private val frameReferenceTimeNanos = System.nanoTime() + private var layoutVersionCounter = 0 - private var committedResolveVersion = UNSET private var committedLayoutVersion = UNSET + private var committedLayoutFrameId = UNSET private var sizeConstraints: SizeConstraints = SizeConstraints() private var hasSizeConstraints = false private val resolveToken = Any() @@ -140,6 +148,11 @@ constructor( uiHandler.postAtTime({ requestResolve(null, async) }, resolveToken, 0) } + private fun getElapsedFrameCount(): Int { + val elapsedTimeNanos = System.nanoTime() - frameReferenceTimeNanos + return (elapsedTimeNanos * 1.0 / normalVSyncTime).roundToInt() + } + private fun requestResolve( resolveFunc: ResolveFunc?, doAsync: Boolean, @@ -151,6 +164,7 @@ constructor( if (resolveFunc == null && pendingStateUpdates.isEmpty()) { return } + if (resolveFunc != null) { latestResolveFunc = resolveFunc } @@ -161,7 +175,8 @@ constructor( committedResolvedTree, committedState, if (pendingStateUpdates.isEmpty()) emptyList() else ArrayList(pendingStateUpdates), - resolveVersionCounter++) + resolveVersionCounter++, + getElapsedFrameCount()) resolveFuture = future } if (doAsync) { @@ -193,6 +208,7 @@ constructor( if (future.version > committedResolveVersion) { committedResolveVersion = future.version committedResolvedTree = result.resolvedNode + committedResolvedFrameId = future.frameId committedState = result.resolvedState pendingStateUpdates.removeAll(future.stateUpdatesToApply) didCommit = true @@ -223,6 +239,7 @@ constructor( commitedTree, committedState, layoutVersionCounter++, + committedResolvedFrameId, committedRenderResult, extensions, sizeConstraints) @@ -238,6 +255,7 @@ constructor( committedRenderResult != renderResult) { committedLayoutVersion = layoutFuture.version committedNewLayout = true + committedLayoutFrameId = layoutFuture.frameId committedRenderResult = renderResult } if (this.layoutFuture == layoutFuture) { @@ -340,7 +358,8 @@ constructor( @ThreadConfined(ThreadConfined.UI) private fun maybePromoteCommittedTreeToUI() { synchronized(this) { - delegate.commitToUI(committedRenderResult?.renderTree, committedRenderResult?.state) + delegate.commitToUI( + committedRenderResult?.renderTree, committedRenderResult?.state, committedLayoutFrameId) if (uiRenderTree == committedRenderResult?.renderTree) { return } diff --git a/litho-rendercore/src/main/java/com/facebook/rendercore/ResolveFuture.kt b/litho-rendercore/src/main/java/com/facebook/rendercore/ResolveFuture.kt index bca5fd77d65..6e4cbbfed19 100644 --- a/litho-rendercore/src/main/java/com/facebook/rendercore/ResolveFuture.kt +++ b/litho-rendercore/src/main/java/com/facebook/rendercore/ResolveFuture.kt @@ -26,7 +26,8 @@ class ResolveFuture>( committedTree: Node?, committedState: State?, val stateUpdatesToApply: List, - val version: Int + val version: Int, + val frameId: Int, ) : ThreadInheritingPriorityFuture, State>>( Callable { diff --git a/litho-rendercore/src/main/java/com/facebook/rendercore/utils/VSyncUtils.kt b/litho-rendercore/src/main/java/com/facebook/rendercore/utils/VSyncUtils.kt new file mode 100644 index 00000000000..368362d13f5 --- /dev/null +++ b/litho-rendercore/src/main/java/com/facebook/rendercore/utils/VSyncUtils.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * 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.facebook.rendercore.utils + +import android.content.Context +import android.view.WindowManager +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.roundToInt + +object VSyncUtils { + private const val DEFAULT_REFRESH_RATE = 60.0 + private const val REFRESH_RATE_MIN = 30.0 + private const val REFRESH_RATE_MAX = 240.0 + private val ONE_SECOND_IN_NS = TimeUnit.SECONDS.toNanos(1) + + private val vsyncTimeNs: AtomicInteger = AtomicInteger(-1) + + @JvmStatic + fun getNormalVsyncTime(context: Context): Int { + var value = vsyncTimeNs.get() + if (value != -1) { + return value + } + + // Initialize the value. + val display = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + var refreshRate = display.refreshRate.toDouble() + + // If refresh rate is lower than 0, it means the OS is not reporting a correct value. We + // will + // assume it is 60. + refreshRate = + if (refreshRate < 0) { + DEFAULT_REFRESH_RATE + } else { + // Cap refresh rates between 30 and 240. Anything else is unreasonable. + refreshRate.coerceIn(REFRESH_RATE_MIN, REFRESH_RATE_MAX) + } + + value = (ONE_SECOND_IN_NS / refreshRate).roundToInt() + vsyncTimeNs.compareAndSet(-1, value) + + return value + } +} diff --git a/litho-rendercore/src/test/java/com/facebook/rendercore/RenderStateTest.kt b/litho-rendercore/src/test/java/com/facebook/rendercore/RenderStateTest.kt index b734c376481..fa3edc77077 100644 --- a/litho-rendercore/src/test/java/com/facebook/rendercore/RenderStateTest.kt +++ b/litho-rendercore/src/test/java/com/facebook/rendercore/RenderStateTest.kt @@ -46,7 +46,7 @@ class RenderStateTest { nextState: Any? ) = Unit - override fun commitToUI(tree: RenderTree?, state: Any?) = Unit + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) = Unit } @LooperMode(LooperMode.Mode.PAUSED) @@ -74,7 +74,7 @@ class RenderStateTest { nextState: Any? ) = Unit - override fun commitToUI(tree: RenderTree?, state: Any?) = Unit + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) = Unit }, null, null, @@ -110,7 +110,7 @@ class RenderStateTest { wasCalled.set(true) } - override fun commitToUI(tree: RenderTree?, state: Any?) = Unit + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) = Unit }, null, null) @@ -145,7 +145,7 @@ class RenderStateTest { } } - override fun commitToUI(tree: RenderTree?, state: Any?) = Unit + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) = Unit }, null, null) @@ -293,7 +293,7 @@ class RenderStateTest { numberOfCommits.incrementAndGet() } - override fun commitToUI(tree: RenderTree?, state: Any?) { + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) { numberOfUICommits.incrementAndGet() } }, @@ -331,7 +331,7 @@ class RenderStateTest { numberOfCommits.incrementAndGet() } - override fun commitToUI(tree: RenderTree?, state: Any?) { + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) { numberOfUICommits.incrementAndGet() } }, diff --git a/litho-rendercore/src/test/java/com/facebook/rendercore/RootHostViewTests.kt b/litho-rendercore/src/test/java/com/facebook/rendercore/RootHostViewTests.kt index 15129d1d134..8f9c5305182 100644 --- a/litho-rendercore/src/test/java/com/facebook/rendercore/RootHostViewTests.kt +++ b/litho-rendercore/src/test/java/com/facebook/rendercore/RootHostViewTests.kt @@ -53,7 +53,7 @@ class RootHostViewTests { nextState: Any? ) = Unit - override fun commitToUI(tree: RenderTree?, state: Any?) = Unit + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) = Unit }, null, null) @@ -84,7 +84,7 @@ class RootHostViewTests { nextState: Any? ) = Unit - override fun commitToUI(tree: RenderTree?, state: Any?) = Unit + override fun commitToUI(tree: RenderTree?, state: Any?, frameId: Int) = Unit }, null, null)