Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext) :

preOperations.executeBatch(batchNumber, nodesManager)
operations.executeBatch(batchNumber, nodesManager)

// Operations executed above may have started animations (e.g. startAnimatingNode); the
// frame callback disarms itself when no animations are active, so re-arm it here.
// didDispatchMountItems is UI-confined, like enqueueFrameCallback.
enqueueFrameCallback()
}

// For non-FabricUIManager only (no-op since Fabric is the only supported UIManager)
Expand Down Expand Up @@ -348,9 +353,11 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext) :
val nodesManager = nodesManager ?: return
if (nodesManager.hasActiveAnimations()) {
nodesManager.runUpdates(frameTimeNanos)
// Only keep the Choreographer armed while animations are actually running.
// didDispatchMountItems re-arms this callback when new animation operations
// (e.g. startAnimatingNode) execute.
enqueueFrameCallback()
}

enqueueFrameCallback()
} catch (ex: Exception) {
throw RuntimeException(ex)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1663,7 +1663,12 @@ public void doFrameGuarded(long frameTimeNanos) {
mIsMountingEnabled = false;
throw ex;
} finally {
schedule();
// Keep the Choreographer armed only while items remain pending; posting new items
// calls schedule() directly. An unconditional re-schedule here kept the Choreographer
// running at vsync rate while idle.
if (mMountItemDispatcher.hasPendingItems()) {
schedule();
}
}

if (ReactNativeFeatureFlags.useSharedAnimatedBackend() && mBinding != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ internal class MountItemDispatcher(
private val mountItems: Queue<MountItem> = ConcurrentLinkedQueue()
private val preMountItems: Queue<MountItem> = ConcurrentLinkedQueue()

/** @return true if any mount items, pre-mount items or view commands are still pending */
fun hasPendingItems(): Boolean =
!viewCommandMountItems.isEmpty() || !mountItems.isEmpty() || !preMountItems.isEmpty()

private var inDispatch: Boolean = false
var batchedExecutionTime: Long = 0L
private set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ public open class JavaTimerManager(
synchronized(timerGuard) {
timers.add(timer)
timerIdsToTimers.put(timerId, timer)
if (!frameCallbackPosted && !isPaused.get()) {
// Re-arm the timers frame callback lazily: it disarms itself whenever the queue drains.
reactChoreographer.postFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS,
timerFrameCallback,
)
frameCallbackPosted = true
}
}
}

Expand Down Expand Up @@ -292,6 +300,7 @@ public open class JavaTimerManager(
return
}
val frameTimeMillis = frameTimeNanos / 1000000
var shouldRepost: Boolean
synchronized(timerGuard) {
while (!timers.isEmpty() && timers.peek()!!.targetTime < frameTimeMillis) {
var timer = timers.poll()
Expand All @@ -309,12 +318,20 @@ public open class JavaTimerManager(
timerIdsToTimers.remove(timer.timerId)
}
}
shouldRepost = timers.isNotEmpty()
if (!shouldRepost) {
// The timer queue is empty: disarm instead of re-posting this callback at vsync rate.
// createTimer re-arms the callback when a new timer arrives.
frameCallbackPosted = false
}
}
timersToCall?.let { timers ->
javaScriptTimerExecutor.callTimers(timers)
timersToCall = null
}
reactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this)
if (shouldRepost) {
reactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,13 @@ internal class FabricEventDispatcher(
override fun doFrame(frameTimeNanos: Long) {
UiThreadUtil.assertOnUiThread()

// This callback is one-shot per schedule request (see maybeDispatchBatchedEvents): events
// are dispatched synchronously in dispatchEvent, so there is nothing to re-post here.
// Re-posting unconditionally kept the Choreographer armed at vsync rate while idle.
isFrameCallbackDispatchScheduled = false

if (shouldStop) {
isFrameCallbackDispatchScheduled = false
} else {
dispatchBatchedEvents()
return
}

Systrace.beginSection(Systrace.TRACE_TAG_REACT, "BatchEventDispatchedListeners")
Expand Down
44 changes: 27 additions & 17 deletions packages/rn-tester/js/examples/Playground/RNTesterPlayground.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,39 @@

import type {RNTesterModuleExample} from '../../types/RNTesterTypes';

import RNTesterText from '../../components/RNTesterText';
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import {View} from 'react-native';

/**
* Reproducer for #58367 — do NOT merge, reproducer only.
*
* Renders nothing but an empty View: the framework itself keeps the
* main-thread Choreographer armed at ~60 doFrames/s while this screen is
* foreground-idle, with zero frames rendered. Verified on API 28/29/36.
*
* To observe on any Android device/emulator with RNTester running this
* playground:
*
* adb shell atrace -t 10 -b 32768 view input -z -o /data/local/tmp/idle.atrace.gz
* adb pull /data/local/tmp/idle.atrace.gz .
* # decompress, then: grep "Choreographer#doFrame" idle.text | wc -l
* # -> ~600 sections for the app pid in 10 idle seconds, each containing
* only an empty "animation" stage (no layout, no draw)
*
* adb shell dumpsys gfxinfo <rn_tester_pkg> reset && sleep 10 \
* && adb shell dumpsys gfxinfo <rn_tester_pkg> | grep "Total frames rendered"
* # -> 0 (the loop renders nothing)
*
* Control: a plain native Activity rendering an empty View receives 0
* doFrames at idle.
*/
function Playground() {
return (
<View style={styles.container}>
<RNTesterText>
Edit "RNTesterPlayground.js" to change this file
</RNTesterText>
</View>
);
return <View style={{flex: 1, backgroundColor: '#000000'}} />;
}

const styles = StyleSheet.create({
container: {
padding: 10,
},
});

export default {
export default ({
title: 'Playground',
name: 'playground',
description: 'Test out new features and ideas.',
render: (): React.Node => <Playground />,
} as RNTesterModuleExample;
}: RNTesterModuleExample);
Loading