You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(android): stop idle frame callbacks on the new backend - #416
Fixes#413. On the new Android backend, the view's render loop and the CommandQueue poll both re-posted a Choreographer callback every vsync for the life of the process, so the app kept waking on every frame even when views were paused, settled, unmounted or the app was backgrounded.
The view now requests one frame at a time, only while it is visible and has something to advance or draw, mirroring how rive-android's Rive composable suspends while settled. Play, configure, rebind, surface changes, pointer input and visibility changes wake it. Each wake re-arms rive-android's settle signal (unsettleStateMachine is internal, so it is reached by reflection, like DeferredRiveWorker), which otherwise fires only once per state machine.
Polling uses rive-android's CommandQueue.beginPolling bound to ProcessLifecycleOwner, so it stops while the app is in the background. A JS call made while backgrounded resolves when the app resumes.
A view-model write from JS after the state machine settled was never applied, because nothing advanced it again. Every write now wakes the live views. Upstream's composable uses ViewModelInstance.dirtyFlow for this, but that only covers the bound instance, not nested (viewModel(path)) or list item instances, so the setters wake the views instead. settled-property.harness.tsx covers it and fails without this change; legacy Android has the same bug, so the test is skipped there.
Measured on an emulator release build with the app backgrounded: Choreographer#doFrame went from ~600 per 10 s (with a Rive view mounted, or after one had been) to 0. A settled view renders no frames, also after JS writes to it. The example app has a new "Idle frame callbacks (#413)" page with pause/unmount controls and a live update counter.
The view's render loop and the CommandQueue poll both re-posted a
Choreographer callback every vsync for the life of the process, so the
app woke on every frame even when every view was paused, settled,
unmounted or backgrounded (#413).
The view now requests one frame at a time, only while it is visible and
has something to advance or draw, like rive-android's Rive composable
suspending while settled. It wakes on play, configure, rebind, surface
changes, pointer input and visibility changes. Each wake re-arms
rive-android's settle signal (internal, reached by reflection), which
otherwise fires only once per state machine.
Polling uses rive-android's CommandQueue.beginPolling bound to the
process lifecycle, so it stops while the app is in the background.
A view-model write from JS after the state machine settled was never
applied, because nothing advanced it again; every write now wakes the
live views. Covered by settled-property.harness.tsx, which fails without
this change.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
A critical state-machine wake race can use a deleted handle, and mismatched rendering and polling lifecycle boundaries can leave views drawing without callbacks.
beginPolling() stops outside RESUMED, but canRender() still allows a visible, paused activity to render, such as one behind another app's translucent activity. This leaves views advancing and drawing without delivery of property listeners, async-read results or settlement notifications. Align rendering and polling to the same lifecycle boundary, and wake views when that lifecycle becomes active again.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Rendering can continue in a paused but visible activity after lifecycle-based polling stops, leaving settlement signals and JS responses unconsumed and preventing the view from settling.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Gate rendering to the lifecycle used by the poller
A paused activity can remain visible in picture-in-picture or split-screen, so this check allows rendering after the ProcessLifecycleOwner-based poller in HybridRiveFileFactory.kt:123 stops. Advances and draws continue, but settlement signals and JS responses are not consumed, preventing the view from settling. Gate rendering on the same lifecycle as polling and wake it on resume, or keep polling active while a visible host is rendering.
Re the previously missed issue in Copilot's last review (polling gated on RESUMED while a paused-but-visible activity keeps rendering): fixed in 3cb01d0. The poller now follows a lifecycle that reports RESUMED whenever the process is at least STARTED, so polling continues for picture-in-picture hosts and still stops once the app is backgrounded. Checked on an emulator: the app gets 0 frame callbacks while backgrounded, and polling and JS writes work again after it returns to the foreground.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed to determine how the missing fixture-build command affects reproducibility of the new regression test and reproducer.
No open issues 🎉
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #413. On the new Android backend, the view's render loop and the
CommandQueuepoll both re-posted a Choreographer callback every vsync for the life of the process, so the app kept waking on every frame even when views were paused, settled, unmounted or the app was backgrounded.Rivecomposable suspends while settled. Play, configure, rebind, surface changes, pointer input and visibility changes wake it. Each wake re-arms rive-android's settle signal (unsettleStateMachineis internal, so it is reached by reflection, likeDeferredRiveWorker), which otherwise fires only once per state machine.CommandQueue.beginPollingbound toProcessLifecycleOwner, so it stops while the app is in the background. A JS call made while backgrounded resolves when the app resumes.ViewModelInstance.dirtyFlowfor this, but that only covers the bound instance, not nested (viewModel(path)) or list item instances, so the setters wake the views instead.settled-property.harness.tsxcovers it and fails without this change; legacy Android has the same bug, so the test is skipped there.Measured on an emulator release build with the app backgrounded:
Choreographer#doFramewent from ~600 per 10 s (with a Rive view mounted, or after one had been) to 0. A settled view renders no frames, also after JS writes to it. The example app has a new "Idle frame callbacks (#413)" page with pause/unmount controls and a live update counter.