From 6a3e1402ed2d66d2c41d6d029906567ed4fbe869 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Mon, 23 Jun 2025 05:35:40 -0700 Subject: [PATCH 1/2] Convert `EventDispatcherImpl` to Kotlin Summary: This is another class moving from Java to Kotlin. Changelog: [Internal] [Changed] - Differential Revision: D77021952 --- .../ReactAndroid/api/ReactAndroid.api | 6 +- .../facebook/react/uimanager/events/Event.kt | 2 +- .../uimanager/events/EventDispatcherImpl.java | 372 ------------------ .../uimanager/events/EventDispatcherImpl.kt | 334 ++++++++++++++++ 4 files changed, 340 insertions(+), 374 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index f13d40d02b3314..38e82384e6401e 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4866,7 +4866,8 @@ public abstract interface class com/facebook/react/uimanager/events/EventDispatc public abstract fun removeListener (Lcom/facebook/react/uimanager/events/EventDispatcherListener;)V } -public class com/facebook/react/uimanager/events/EventDispatcherImpl : com/facebook/react/bridge/LifecycleEventListener, com/facebook/react/uimanager/events/EventDispatcher { +public final class com/facebook/react/uimanager/events/EventDispatcherImpl : com/facebook/react/bridge/LifecycleEventListener, com/facebook/react/uimanager/events/EventDispatcher { + public static final field Companion Lcom/facebook/react/uimanager/events/EventDispatcherImpl$Companion; public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun addBatchEventDispatchedListener (Lcom/facebook/react/uimanager/events/BatchEventDispatchedListener;)V public fun addListener (Lcom/facebook/react/uimanager/events/EventDispatcherListener;)V @@ -4880,6 +4881,9 @@ public class com/facebook/react/uimanager/events/EventDispatcherImpl : com/faceb public fun removeListener (Lcom/facebook/react/uimanager/events/EventDispatcherListener;)V } +public final class com/facebook/react/uimanager/events/EventDispatcherImpl$Companion { +} + public abstract interface class com/facebook/react/uimanager/events/EventDispatcherListener { public abstract fun onEventDispatch (Lcom/facebook/react/uimanager/events/Event;)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt index 86362723eb1de4..d90bcdc98afe0f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt @@ -95,7 +95,7 @@ public abstract class Event> { * Two events will only ever try to be coalesced if they have the same event name, view id, and * coalescing key. */ - public open fun coalesce(otherEvent: Event?): Event? = + public open fun coalesce(otherEvent: Event<*>?): Event<*>? = if (timestampMs >= otherEvent?.timestampMs ?: 0) this else otherEvent /** diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.java deleted file mode 100644 index 8db41b54d215d3..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.uimanager.events; - -import android.util.LongSparseArray; -import android.view.Choreographer; -import com.facebook.infer.annotation.Assertions; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.MapBuilder; -import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture; -import com.facebook.react.modules.core.ReactChoreographer; -import com.facebook.systrace.Systrace; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Class responsible for dispatching UI events to JS. The main purpose of this class is to act as an - * intermediary between UI code generating events and JS, making sure we don't send more events than - * JS can process. - * - *

To use it, create a subclass of {@link Event} and call {@link #dispatchEvent(Event)} whenever - * there's a UI event to dispatch. - * - *

This class works by installing a Choreographer frame callback on the main thread. This - * callback then enqueues a runnable on the JS thread (if one is not already pending) that is - * responsible for actually dispatch events to JS. This implementation depends on the properties - * that 1) FrameCallbacks run after UI events have been processed in Choreographer.java 2) when we - * enqueue a runnable on the JS queue thread, it won't be called until after any previously enqueued - * JS jobs have finished processing - * - *

If JS is taking a long time processing events, then the UI events generated on the UI thread - * can be coalesced into fewer events so that when the runnable runs, we don't overload JS with a - * ton of events and make it get even farther behind. - * - *

Ideally, we don't need this and JS is fast enough to process all the events each frame, but - * bad things happen, including load on CPUs from the system, and we should handle this case well. - * - *

== Event Cookies == - * - *

An event cookie is made up of the event type id, view tag, and a custom coalescing key. Only - * Events that have the same cookie can be coalesced. - * - *

Event Cookie Composition: VIEW_TAG_MASK = 0x00000000ffffffff EVENT_TYPE_ID_MASK = - * 0x0000ffff00000000 COALESCING_KEY_MASK = 0xffff000000000000 - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -@InteropLegacyArchitecture -public class EventDispatcherImpl implements EventDispatcher, LifecycleEventListener { - - private static final Comparator EVENT_COMPARATOR = - new Comparator() { - @Override - public int compare(Event lhs, Event rhs) { - if (lhs == null && rhs == null) { - return 0; - } - if (lhs == null) { - return -1; - } - if (rhs == null) { - return 1; - } - - long diff = lhs.getTimestampMs() - rhs.getTimestampMs(); - if (diff == 0) { - return 0; - } else if (diff < 0) { - return -1; - } else { - return 1; - } - } - }; - - private final Object mEventsStagingLock = new Object(); - private final Object mEventsToDispatchLock = new Object(); - private final ReactApplicationContext mReactContext; - private final LongSparseArray mEventCookieToLastEventIdx = new LongSparseArray<>(); - private final Map mEventNameToEventId = MapBuilder.newHashMap(); - private final DispatchEventsRunnable mDispatchEventsRunnable = new DispatchEventsRunnable(); - private final ArrayList mEventStaging = new ArrayList<>(); - private final CopyOnWriteArrayList mListeners = - new CopyOnWriteArrayList<>(); - private final CopyOnWriteArrayList mPostEventDispatchListeners = - new CopyOnWriteArrayList<>(); - private final ScheduleDispatchFrameCallback mCurrentFrameCallback = - new ScheduleDispatchFrameCallback(); - private final AtomicInteger mHasDispatchScheduledCount = new AtomicInteger(); - - private Event[] mEventsToDispatch = new Event[16]; - private int mEventsToDispatchSize = 0; - private final EventEmitterImpl mReactEventEmitter; - private short mNextEventTypeId = 0; - private volatile boolean mHasDispatchScheduled = false; - - public EventDispatcherImpl(ReactApplicationContext reactContext) { - mReactContext = reactContext; - mReactContext.addLifecycleEventListener(this); - mReactEventEmitter = new EventEmitterImpl(mReactContext); - } - - /** Sends the given Event to JS, coalescing eligible events if JS is backed up. */ - public void dispatchEvent(Event event) { - Assertions.assertCondition(event.isInitialized(), "Dispatched event hasn't been initialized"); - - for (EventDispatcherListener listener : mListeners) { - listener.onEventDispatch(event); - } - - synchronized (mEventsStagingLock) { - mEventStaging.add(event); - Systrace.startAsyncFlow(Systrace.TRACE_TAG_REACT, event.getEventName(), event.getUniqueID()); - } - maybePostFrameCallbackFromNonUI(); - } - - public void dispatchAllEvents() { - maybePostFrameCallbackFromNonUI(); - } - - private void maybePostFrameCallbackFromNonUI() { - mCurrentFrameCallback.maybePostFromNonUI(); - } - - /** Add a listener to this EventDispatcher. */ - public void addListener(EventDispatcherListener listener) { - mListeners.add(listener); - } - - /** Remove a listener from this EventDispatcher. */ - public void removeListener(EventDispatcherListener listener) { - mListeners.remove(listener); - } - - public void addBatchEventDispatchedListener(BatchEventDispatchedListener listener) { - mPostEventDispatchListeners.add(listener); - } - - public void removeBatchEventDispatchedListener(BatchEventDispatchedListener listener) { - mPostEventDispatchListeners.remove(listener); - } - - @Override - public void onHostResume() { - maybePostFrameCallbackFromNonUI(); - } - - @Override - public void onHostPause() { - stopFrameCallback(); - } - - @Override - public void onHostDestroy() { - stopFrameCallback(); - } - - @Override - @Deprecated - public void onCatalystInstanceDestroyed() { - UiThreadUtil.runOnUiThread(this::stopFrameCallback); - } - - private void stopFrameCallback() { - UiThreadUtil.assertOnUiThread(); - mCurrentFrameCallback.stop(); - } - - /** - * We use a staging data structure so that all UI events generated in a single frame are - * dispatched at once. Otherwise, a JS runnable enqueued in a previous frame could run while the - * UI thread is in the process of adding UI events and we might incorrectly send one event this - * frame and another from this frame during the next. - */ - private void moveStagedEventsToDispatchQueue() { - synchronized (mEventsStagingLock) { - synchronized (mEventsToDispatchLock) { - for (int i = 0; i < mEventStaging.size(); i++) { - Event event = mEventStaging.get(i); - - if (!event.canCoalesce()) { - addEventToEventsToDispatch(event); - continue; - } - - long eventCookie = - getEventCookie(event.getViewTag(), event.getEventName(), event.getCoalescingKey()); - - Event eventToAdd = null; - Event eventToDispose = null; - Integer lastEventIdx = mEventCookieToLastEventIdx.get(eventCookie); - - if (lastEventIdx == null) { - eventToAdd = event; - mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize); - } else { - Event lastEvent = mEventsToDispatch[lastEventIdx]; - Event coalescedEvent = event.coalesce(lastEvent); - if (coalescedEvent != lastEvent) { - eventToAdd = coalescedEvent; - mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize); - eventToDispose = lastEvent; - mEventsToDispatch[lastEventIdx] = null; - } else { - eventToDispose = event; - } - } - - if (eventToAdd != null) { - addEventToEventsToDispatch(eventToAdd); - } - if (eventToDispose != null) { - eventToDispose.dispose(); - } - } - } - mEventStaging.clear(); - } - } - - private long getEventCookie(int viewTag, String eventName, short coalescingKey) { - short eventTypeId; - Short eventIdObj = mEventNameToEventId.get(eventName); - if (eventIdObj != null) { - eventTypeId = eventIdObj; - } else { - eventTypeId = mNextEventTypeId++; - mEventNameToEventId.put(eventName, eventTypeId); - } - return getEventCookie(viewTag, eventTypeId, coalescingKey); - } - - private static long getEventCookie(int viewTag, short eventTypeId, short coalescingKey) { - return viewTag - | (((long) eventTypeId) & 0xffff) << 32 - | (((long) coalescingKey) & 0xffff) << 48; - } - - private class ScheduleDispatchFrameCallback implements Choreographer.FrameCallback { - private volatile boolean mIsPosted = false; - private boolean mShouldStop = false; - - @Override - public void doFrame(long frameTimeNanos) { - UiThreadUtil.assertOnUiThread(); - - if (mShouldStop) { - mIsPosted = false; - } else { - post(); - } - - Systrace.beginSection(Systrace.TRACE_TAG_REACT, "ScheduleDispatchFrameCallback"); - try { - moveStagedEventsToDispatchQueue(); - - if (!mHasDispatchScheduled) { - mHasDispatchScheduled = true; - Systrace.startAsyncFlow( - Systrace.TRACE_TAG_REACT, - "ScheduleDispatchFrameCallback", - mHasDispatchScheduledCount.get()); - mReactContext.runOnJSQueueThread(mDispatchEventsRunnable); - } - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT); - } - } - - public void stop() { - mShouldStop = true; - } - - public void maybePost() { - if (!mIsPosted) { - mIsPosted = true; - post(); - } - } - - private void post() { - ReactChoreographer.getInstance() - .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, mCurrentFrameCallback); - } - - public void maybePostFromNonUI() { - if (mIsPosted) { - return; - } - - // We should only hit this slow path when we receive events while the host activity is paused. - if (mReactContext.isOnUiQueueThread()) { - maybePost(); - } else { - mReactContext.runOnUiQueueThread( - new Runnable() { - @Override - public void run() { - maybePost(); - } - }); - } - } - } - - private class DispatchEventsRunnable implements Runnable { - - @Override - public void run() { - Systrace.beginSection(Systrace.TRACE_TAG_REACT, "DispatchEventsRunnable"); - try { - Systrace.endAsyncFlow( - Systrace.TRACE_TAG_REACT, - "ScheduleDispatchFrameCallback", - mHasDispatchScheduledCount.getAndIncrement()); - mHasDispatchScheduled = false; - synchronized (mEventsToDispatchLock) { - if (mEventsToDispatchSize > 0) { - // We avoid allocating an array and iterator, and "sorting" if we don't need to. - // This occurs when the size of mEventsToDispatch is zero or one. - if (mEventsToDispatchSize > 1) { - Arrays.sort(mEventsToDispatch, 0, mEventsToDispatchSize, EVENT_COMPARATOR); - } - for (int eventIdx = 0; eventIdx < mEventsToDispatchSize; eventIdx++) { - Event event = mEventsToDispatch[eventIdx]; - // Event can be null if it has been coalesced into another event. - if (event == null) { - continue; - } - Systrace.endAsyncFlow( - Systrace.TRACE_TAG_REACT, event.getEventName(), event.getUniqueID()); - - event.dispatchModern(mReactEventEmitter); - event.dispose(); - } - clearEventsToDispatch(); - mEventCookieToLastEventIdx.clear(); - } - } - for (BatchEventDispatchedListener listener : mPostEventDispatchListeners) { - listener.onBatchEventDispatched(); - } - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT); - } - } - } - - private void addEventToEventsToDispatch(Event event) { - if (mEventsToDispatchSize == mEventsToDispatch.length) { - mEventsToDispatch = Arrays.copyOf(mEventsToDispatch, 2 * mEventsToDispatch.length); - } - mEventsToDispatch[mEventsToDispatchSize++] = event; - } - - private void clearEventsToDispatch() { - Arrays.fill(mEventsToDispatch, 0, mEventsToDispatchSize, null); - mEventsToDispatchSize = 0; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt new file mode 100644 index 00000000000000..3e156bfa8cf04e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt @@ -0,0 +1,334 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager.events + +import android.util.LongSparseArray +import android.view.Choreographer +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.UiThreadUtil.assertOnUiThread +import com.facebook.react.bridge.UiThreadUtil.runOnUiThread +import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture +import com.facebook.react.modules.core.ReactChoreographer +import com.facebook.systrace.Systrace +import java.util.Arrays +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicInteger + +/** + * Class responsible for dispatching UI events to JS. The main purpose of this class is to act as an + * intermediary between UI code generating events and JS, making sure we don't send more events than + * JS can process. + * + * To use it, create a subclass of [Event] and call [dispatchEvent] whenever there's a UI event to + * dispatch. + * + * This class works by installing a [Choreographer] frame callback on the main thread. This callback + * then enqueues a runnable on the JS thread (if one is not already pending) that is responsible for + * actually dispatch events to JS. This implementation depends on the properties that + * 1) FrameCallbacks run after UI events have been processed in [Choreographer] + * 2) when we enqueue a runnable on the JS queue thread, it won't be called until after any + * previously enqueued JS jobs have finished processing + * + * If JS is taking a long time processing events, then the UI events generated on the UI thread can + * be coalesced into fewer events so that when the runnable runs, we don't overload JS with a ton of + * events and make it get even farther behind. + * + * Ideally, we don't need this and JS is fast enough to process all the events each frame, but bad + * things happen, including load on CPUs from the system, and we should handle this case well. + * + * # Event Cookies + * + * An event cookie is made up of the event type id, view tag, and a custom coalescing key. Only + * Events that have the same cookie can be coalesced. + * + * Event Cookie Composition: VIEW_TAG_MASK = 0x00000000ffffffff EVENT_TYPE_ID_MASK = + * 0x0000ffff00000000 COALESCING_KEY_MASK = 0xffff000000000000 + */ +@InteropLegacyArchitecture +public class EventDispatcherImpl(private val reactContext: ReactApplicationContext) : + EventDispatcher, LifecycleEventListener { + private val eventsStagingLock = Any() + private val eventsToDispatchLock = Any() + private val eventCookieToLastEventIdx = LongSparseArray() + private val eventNameToEventId: MutableMap = mutableMapOf() + private val dispatchEventsRunnable: DispatchEventsRunnable = DispatchEventsRunnable() + private val eventStaging = ArrayList>() + private val listeners = CopyOnWriteArrayList() + private val postEventDispatchListeners = CopyOnWriteArrayList() + private val currentFrameCallback: ScheduleDispatchFrameCallback = ScheduleDispatchFrameCallback() + private val hasDispatchScheduledCount = AtomicInteger() + private var eventsToDispatch: Array?> = arrayOfNulls(16) + private var eventsToDispatchSize = 0 + private val reactEventEmitter: EventEmitterImpl + private var nextEventTypeId: Short = 0 + @Volatile private var hasDispatchScheduled = false + + init { + reactContext.addLifecycleEventListener(this) + reactEventEmitter = EventEmitterImpl(reactContext) + } + + /** Sends the given Event to JS, coalescing eligible events if JS is backed up. */ + override fun dispatchEvent(event: Event<*>) { + require(event.isInitialized) { "Dispatched event hasn't been initialized" } + + for (listener in listeners) { + listener.onEventDispatch(event) + } + + synchronized(eventsStagingLock) { + eventStaging.add(event) + Systrace.startAsyncFlow(Systrace.TRACE_TAG_REACT, event.getEventName(), event.uniqueID) + } + maybePostFrameCallbackFromNonUI() + } + + override fun dispatchAllEvents() { + maybePostFrameCallbackFromNonUI() + } + + private fun maybePostFrameCallbackFromNonUI() { + currentFrameCallback.maybePostFromNonUI() + } + + /** Add a listener to this EventDispatcher. */ + override fun addListener(listener: EventDispatcherListener) { + listeners.add(listener) + } + + /** Remove a listener from this EventDispatcher. */ + override fun removeListener(listener: EventDispatcherListener) { + listeners.remove(listener) + } + + override fun addBatchEventDispatchedListener(listener: BatchEventDispatchedListener) { + postEventDispatchListeners.add(listener) + } + + override fun removeBatchEventDispatchedListener(listener: BatchEventDispatchedListener) { + postEventDispatchListeners.remove(listener) + } + + override fun onHostResume() { + maybePostFrameCallbackFromNonUI() + } + + override fun onHostPause() { + stopFrameCallback() + } + + override fun onHostDestroy() { + stopFrameCallback() + } + + @Deprecated("Private API, should only be used when the concrete implementation is known.") + override fun onCatalystInstanceDestroyed() { + runOnUiThread { this.stopFrameCallback() } + } + + private fun stopFrameCallback() { + assertOnUiThread() + currentFrameCallback.stop() + } + + /** + * We use a staging data structure so that all UI events generated in a single frame are + * dispatched at once. Otherwise, a JS runnable enqueued in a previous frame could run while the + * UI thread is in the process of adding UI events and we might incorrectly send one event this + * frame and another from this frame during the next. + */ + private fun moveStagedEventsToDispatchQueue() { + synchronized(eventsStagingLock) { + synchronized(eventsToDispatchLock) { + for (i in eventStaging.indices) { + val event: Event<*> = eventStaging[i] + + if (!event.canCoalesce()) { + addEventToEventsToDispatch(event) + continue + } + + val eventCookie = + getEventCookie(event.viewTag, event.getEventName(), event.getCoalescingKey()) + + var eventToAdd: Event<*>? = null + var eventToDispose: Event<*>? = null + val lastEventIdx = eventCookieToLastEventIdx[eventCookie] + + if (lastEventIdx == null) { + eventToAdd = event + eventCookieToLastEventIdx.put(eventCookie, eventsToDispatchSize) + } else { + val lastEvent: Event<*> = checkNotNull(eventsToDispatch[lastEventIdx]) + val coalescedEvent = event.coalesce(lastEvent) + if (coalescedEvent !== lastEvent) { + eventToAdd = coalescedEvent + eventCookieToLastEventIdx.put(eventCookie, eventsToDispatchSize) + eventToDispose = lastEvent + eventsToDispatch[lastEventIdx] = null + } else { + eventToDispose = event + } + } + + if (eventToAdd != null) { + addEventToEventsToDispatch(eventToAdd) + } + eventToDispose?.dispose() + } + } + eventStaging.clear() + } + } + + private fun getEventCookie(viewTag: Int, eventName: String, coalescingKey: Short): Long { + val eventTypeId: Short + val eventIdObj = eventNameToEventId[eventName] + if (eventIdObj != null) { + eventTypeId = eventIdObj + } else { + eventTypeId = nextEventTypeId++ + eventNameToEventId[eventName] = eventTypeId + } + return getEventCookie(viewTag, eventTypeId, coalescingKey) + } + + private inner class ScheduleDispatchFrameCallback : Choreographer.FrameCallback { + @Volatile private var isPosted = false + private var shouldStop = false + + override fun doFrame(frameTimeNanos: Long) { + assertOnUiThread() + + if (shouldStop) { + isPosted = false + } else { + post() + } + + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "ScheduleDispatchFrameCallback") + try { + moveStagedEventsToDispatchQueue() + + if (!hasDispatchScheduled) { + hasDispatchScheduled = true + Systrace.startAsyncFlow( + Systrace.TRACE_TAG_REACT, + "ScheduleDispatchFrameCallback", + hasDispatchScheduledCount.get()) + reactContext.runOnJSQueueThread(dispatchEventsRunnable) + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT) + } + } + + fun stop() { + shouldStop = true + } + + fun maybePost() { + if (!isPosted) { + isPosted = true + post() + } + } + + fun post() { + ReactChoreographer.getInstance() + .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, currentFrameCallback) + } + + fun maybePostFromNonUI() { + if (isPosted) { + return + } + + // We should only hit this slow path when we receive events while the host activity is paused. + if (reactContext.isOnUiQueueThread) { + maybePost() + } else { + reactContext.runOnUiQueueThread { maybePost() } + } + } + } + + private inner class DispatchEventsRunnable : Runnable { + override fun run() { + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "DispatchEventsRunnable") + try { + Systrace.endAsyncFlow( + Systrace.TRACE_TAG_REACT, + "ScheduleDispatchFrameCallback", + hasDispatchScheduledCount.getAndIncrement()) + hasDispatchScheduled = false + synchronized(eventsToDispatchLock) { + if (eventsToDispatchSize > 0) { + // We avoid allocating an array and iterator, and "sorting" if we don't need to. + // This occurs when the size of mEventsToDispatch is zero or one. + if (eventsToDispatchSize > 1) { + Arrays.sort(eventsToDispatch, 0, eventsToDispatchSize, EVENT_COMPARATOR) + } + for (eventIdx in 0..) { + if (eventsToDispatchSize == eventsToDispatch.size) { + eventsToDispatch = eventsToDispatch.copyOf(2 * eventsToDispatch.size) + } + eventsToDispatch[eventsToDispatchSize++] = event + } + + private fun clearEventsToDispatch() { + Arrays.fill(eventsToDispatch, 0, eventsToDispatchSize, null) + eventsToDispatchSize = 0 + } + + public companion object { + private val EVENT_COMPARATOR: java.util.Comparator?> = + java.util.Comparator { lhs, rhs -> + when { + lhs == null && rhs == null -> 0 + lhs == null -> -1 + rhs == null -> 1 + else -> { + val diff = lhs.timestampMs - rhs.timestampMs + when { + diff == 0L -> 0 + diff < 0 -> -1 + else -> 1 + } + } + } + } + + private fun getEventCookie(viewTag: Int, eventTypeId: Short, coalescingKey: Short): Long = + (viewTag.toLong() or + (((eventTypeId.toLong()) and 0xffffL) shl 32) or + (((coalescingKey.toLong()) and 0xffffL) shl 48)) + } +} From 51938af12e1930222f15ba4a41cecdee2ba5d819 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Mon, 23 Jun 2025 05:41:26 -0700 Subject: [PATCH 2/2] Make `EventDispatcherImpl` internal (#52154) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52154 I wasn't able to find any meaningful usage of `EventDispatcherImpl` in OSS, therefore I'm making this class internal. Changelog: [Internal] [Changed] - Reviewed By: javache Differential Revision: D77024759 --- .../ReactAndroid/api/ReactAndroid.api | 18 ------------------ .../uimanager/events/EventDispatcherImpl.kt | 4 ++-- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 38e82384e6401e..b8fc80043ceb0b 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4866,24 +4866,6 @@ public abstract interface class com/facebook/react/uimanager/events/EventDispatc public abstract fun removeListener (Lcom/facebook/react/uimanager/events/EventDispatcherListener;)V } -public final class com/facebook/react/uimanager/events/EventDispatcherImpl : com/facebook/react/bridge/LifecycleEventListener, com/facebook/react/uimanager/events/EventDispatcher { - public static final field Companion Lcom/facebook/react/uimanager/events/EventDispatcherImpl$Companion; - public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V - public fun addBatchEventDispatchedListener (Lcom/facebook/react/uimanager/events/BatchEventDispatchedListener;)V - public fun addListener (Lcom/facebook/react/uimanager/events/EventDispatcherListener;)V - public fun dispatchAllEvents ()V - public fun dispatchEvent (Lcom/facebook/react/uimanager/events/Event;)V - public fun onCatalystInstanceDestroyed ()V - public fun onHostDestroy ()V - public fun onHostPause ()V - public fun onHostResume ()V - public fun removeBatchEventDispatchedListener (Lcom/facebook/react/uimanager/events/BatchEventDispatchedListener;)V - public fun removeListener (Lcom/facebook/react/uimanager/events/EventDispatcherListener;)V -} - -public final class com/facebook/react/uimanager/events/EventDispatcherImpl$Companion { -} - public abstract interface class com/facebook/react/uimanager/events/EventDispatcherListener { public abstract fun onEventDispatch (Lcom/facebook/react/uimanager/events/Event;)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt index 3e156bfa8cf04e..bd033022f77f28 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherImpl.kt @@ -51,7 +51,7 @@ import java.util.concurrent.atomic.AtomicInteger * 0x0000ffff00000000 COALESCING_KEY_MASK = 0xffff000000000000 */ @InteropLegacyArchitecture -public class EventDispatcherImpl(private val reactContext: ReactApplicationContext) : +internal class EventDispatcherImpl(private val reactContext: ReactApplicationContext) : EventDispatcher, LifecycleEventListener { private val eventsStagingLock = Any() private val eventsToDispatchLock = Any() @@ -308,7 +308,7 @@ public class EventDispatcherImpl(private val reactContext: ReactApplicationConte eventsToDispatchSize = 0 } - public companion object { + companion object { private val EVENT_COMPARATOR: java.util.Comparator?> = java.util.Comparator { lhs, rhs -> when {