Skip to content
Open
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 @@ -51,8 +51,8 @@ class EventDispatcher {

/*
* Dispatches a raw event with asynchronous batched priority. Before the
* dispatch we make sure that no other RawEvent of same type and same target
* is on the queue.
* dispatch we make sure that no other RawEvent of same type, same target,
* and same coalescing key is on the queue.
*/
void dispatchUniqueEvent(RawEvent &&rawEvent) const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ void EventEmitter::dispatchUniqueEvent(
eventTimestamp);
}

void EventEmitter::dispatchUniqueEvent(
std::string type,
folly::dynamic&& payload,
HighResTimeStamp eventTimestamp,
RawEvent::CoalescingKey coalescingKey) const {
dispatchUniqueEvent(
std::move(type),
DynamicEventPayload::create(std::move(payload)),
eventTimestamp,
coalescingKey);
}

void EventEmitter::dispatchEvent(
std::string type,
const ValueFactory& payloadFactory,
Expand Down Expand Up @@ -181,6 +193,18 @@ void EventEmitter::dispatchUniqueEvent(
std::string type,
SharedEventPayload payload,
HighResTimeStamp eventTimestamp) const {
dispatchUniqueEvent(
std::move(type),
std::move(payload),
eventTimestamp,
RawEvent::DefaultCoalescingKey);
}

void EventEmitter::dispatchUniqueEvent(
std::string type,
SharedEventPayload payload,
HighResTimeStamp eventTimestamp,
RawEvent::CoalescingKey coalescingKey) const {
TraceSection s("EventEmitter::dispatchUniqueEvent");

auto eventDispatcher = eventDispatcher_.lock();
Expand All @@ -203,7 +227,8 @@ void EventEmitter::dispatchUniqueEvent(
std::move(shadowNodeFamily),
RawEvent::Category::Continuous,
true,
eventTimestamp));
eventTimestamp,
coalescingKey));
}

void EventEmitter::setEnabled(bool enabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,20 @@ class EventEmitter {

void dispatchUniqueEvent(std::string type, folly::dynamic &&payload, HighResTimeStamp eventTimestamp) const;

void dispatchUniqueEvent(
std::string type,
folly::dynamic &&payload,
HighResTimeStamp eventTimestamp,
RawEvent::CoalescingKey coalescingKey) const;

void dispatchUniqueEvent(std::string type, SharedEventPayload payload, HighResTimeStamp eventTimestamp) const;

void dispatchUniqueEvent(
std::string type,
SharedEventPayload payload,
HighResTimeStamp eventTimestamp,
RawEvent::CoalescingKey coalescingKey) const;

private:
friend class UIManagerBinding;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ void EventQueue::enqueueEvent(RawEvent&& rawEvent) const {
// for the same target. If the same target has event types A1, B1
// in the event queue and event A2 occurs. A1 has to stay in the
// queue.
if (it->isUnique && it->type == rawEvent.type) {
if (it->isUnique && it->type == rawEvent.type &&
it->coalescingKey == rawEvent.coalescingKey) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this account for RawEvent::DefaultCoalescingKey?

@sorinc03 sorinc03 Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Published in the renderer-only revision bc06bf5. RawEvent::DefaultCoalescingKey is the ordinary key 0, not a wildcard: default/default events preserve existing coalescing behavior, while a default-key event and a different custom-key event remain separate. Added C++ cases for both plus interleaved-key ordering. Those renderer GTests have not been executed locally; native CI is pending. The 18 passing local Android tests apply to the separate dependent follow-up #59626, not these C++ tests.

repeatedEvent = it;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class EventQueue {

/*
* Enqueues and (probably later) dispatches a given event.
* Deletes last RawEvent from the queue if it has the same type and target.
* Deletes last RawEvent from the queue if it has the same type, target, and
* coalescing key.
* Can be called on any thread.
*/
void enqueueUniqueEvent(RawEvent &&rawEvent) const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ RawEvent::RawEvent(
std::weak_ptr<const ShadowNodeFamily> shadowNodeFamily,
Category category,
bool isUnique,
HighResTimeStamp eventStartTimeStamp)
HighResTimeStamp eventStartTimeStamp,
CoalescingKey coalescingKey)
: type(std::move(type)),
eventPayload(std::move(eventPayload)),
eventTarget(std::move(eventTarget)),
shadowNodeFamily(std::move(shadowNodeFamily)),
category(category),
isUnique(isUnique),
coalescingKey(coalescingKey),
eventStartTimeStamp(eventStartTimeStamp) {}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class ShadowNodeFamily;
* Represents ready-to-dispatch event object.
*/
struct RawEvent {
using CoalescingKey = int;
static constexpr CoalescingKey DefaultCoalescingKey = 0;

/*
* Defines category of a native platform event. This is used to deduce types
* of events for Concurrent Mode.
Expand Down Expand Up @@ -76,7 +79,8 @@ struct RawEvent {
std::weak_ptr<const ShadowNodeFamily> shadowNodeFamily,
Category category = Category::Unspecified,
bool isUnique = false,
HighResTimeStamp eventStartTimeStamp = HighResTimeStamp::now());
HighResTimeStamp eventStartTimeStamp = HighResTimeStamp::now(),
CoalescingKey coalescingKey = DefaultCoalescingKey);

std::string type;
SharedEventPayload eventPayload;
Expand All @@ -85,6 +89,7 @@ struct RawEvent {
Category category;
EventTag loggingTag{0};
bool isUnique{false};
CoalescingKey coalescingKey{DefaultCoalescingKey};

// The timestamp for the event start time. This defaults to the current
// time if not specified by the client (e.g., when MotionEvent was triggered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
#include <jsi/jsi.h>
#include <react/renderer/core/EventLogger.h>
#include <react/renderer/core/EventPipe.h>
#include <react/renderer/core/EventQueue.h>
#include <react/renderer/core/EventQueueProcessor.h>
#include <react/renderer/core/EventTarget.h>
#include <react/renderer/core/InstanceHandle.h>
#include <react/renderer/core/ShadowNodeFamily.h>
#include <react/renderer/core/StatePipe.h>
#include <react/renderer/core/ValueFactoryEventPayload.h>
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>

#include <memory>
#include <stdexcept>
Expand Down Expand Up @@ -65,6 +67,81 @@ class EventQueueProcessorTest : public testing::Test {
ValueFactory dummyValueFactory_;
};

class NoopEventBeat final : public EventBeat {
public:
explicit NoopEventBeat(RuntimeScheduler& runtimeScheduler)
: EventBeat(std::make_shared<EventBeat::OwnerBox>(), runtimeScheduler) {}

void request() const override {}
void requestSynchronous(Tag /*tag*/) const override {}
};

class TestEventQueue final : public EventQueue {
public:
using EventQueue::EventQueue;

void flush(jsi::Runtime& runtime) const {
flushEvents(runtime);
}
};

class EventQueueCoalescingTest : public testing::Test {
protected:
void SetUp() override {
runtime_ = facebook::hermes::makeHermesRuntime();

RuntimeExecutor runtimeExecutor =
[](std::function<void(facebook::jsi::Runtime&)>&& /*callback*/) {};
runtimeScheduler_ = std::make_unique<RuntimeScheduler>(runtimeExecutor);
}

std::unique_ptr<TestEventQueue> createEventQueue() {
auto eventPipe = [this](
jsi::Runtime& /*runtime*/,
const EventTarget* /*eventTarget*/,
const std::string& type,
ReactEventPriority /*priority*/,
const EventPayload& /*payload*/,
HighResTimeStamp eventTimestamp) {
eventTypes_.push_back(type);
eventTimestamps_.push_back(eventTimestamp);
};

auto dummyEventPipeConclusion = [](jsi::Runtime&) {};
auto dummyStatePipe = [](const StateUpdate&) {};
auto mockEventLogger = std::make_shared<MockEventLogger>();

return std::make_unique<TestEventQueue>(
EventQueueProcessor(
eventPipe,
dummyEventPipeConclusion,
dummyStatePipe,
mockEventLogger),
std::make_unique<NoopEventBeat>(*runtimeScheduler_));
}

RawEvent createRawEvent(
std::string type,
RawEvent::CoalescingKey coalescingKey,
HighResTimeStamp eventTimestamp) {
return RawEvent(
std::move(type),
std::make_shared<ValueFactoryEventPayload>(dummyValueFactory_),
nullptr,
{},
RawEvent::Category::Continuous,
false,
eventTimestamp,
coalescingKey);
}

std::unique_ptr<facebook::hermes::HermesRuntime> runtime_;
std::unique_ptr<RuntimeScheduler> runtimeScheduler_;
std::vector<std::string> eventTypes_;
std::vector<HighResTimeStamp> eventTimestamps_;
ValueFactory dummyValueFactory_;
};

TEST_F(EventQueueProcessorTest, singleUnspecifiedEvent) {
eventProcessor_->flushEvents(
*runtime_,
Expand Down Expand Up @@ -205,4 +282,95 @@ TEST_F(EventQueueProcessorTest, releasesEventTargetsWhenDispatchThrows) {
EXPECT_TRUE(eventTarget->getInstanceHandle(*runtime_).isNull());
}

TEST_F(EventQueueCoalescingTest, uniqueEventsWithSameCoalescingKeyCoalesce) {
auto eventQueue = createEventQueue();
auto firstTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(1);
auto secondTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(2);

eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 3, firstTimestamp));
eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 3, secondTimestamp));

eventQueue->flush(*runtime_);

ASSERT_EQ(eventTypes_.size(), 1);
EXPECT_EQ(eventTypes_[0], "topChange");
EXPECT_EQ(eventTimestamps_[0], secondTimestamp);
}

TEST_F(
EventQueueCoalescingTest,
defaultCoalescingKeysPreserveExistingBehavior) {
auto eventQueue = createEventQueue();
auto firstTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(1);
auto secondTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(2);

eventQueue->enqueueUniqueEvent(createRawEvent(
"topChange", RawEvent::DefaultCoalescingKey, firstTimestamp));
eventQueue->enqueueUniqueEvent(createRawEvent(
"topChange", RawEvent::DefaultCoalescingKey, secondTimestamp));
eventQueue->flush(*runtime_);

ASSERT_EQ(eventTimestamps_.size(), 1);
EXPECT_EQ(eventTimestamps_[0], secondTimestamp);
}

TEST_F(EventQueueCoalescingTest, defaultCoalescingKeyDoesNotMatchCustomKey) {
auto eventQueue = createEventQueue();
auto firstTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(1);
auto secondTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(2);

eventQueue->enqueueUniqueEvent(createRawEvent(
"topChange", RawEvent::DefaultCoalescingKey, firstTimestamp));
eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 3, secondTimestamp));
eventQueue->flush(*runtime_);

ASSERT_EQ(eventTimestamps_.size(), 2);
EXPECT_EQ(eventTimestamps_[0], firstTimestamp);
EXPECT_EQ(eventTimestamps_[1], secondTimestamp);
}

TEST_F(EventQueueCoalescingTest, differentKeysPreserveEventOrder) {
auto eventQueue = createEventQueue();
auto firstTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(1);
auto secondTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(2);
auto thirdTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(3);

eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 3, firstTimestamp));
eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 4, secondTimestamp));
eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 3, thirdTimestamp));
eventQueue->flush(*runtime_);

ASSERT_EQ(eventTimestamps_.size(), 3);
EXPECT_EQ(eventTimestamps_[0], firstTimestamp);
EXPECT_EQ(eventTimestamps_[1], secondTimestamp);
EXPECT_EQ(eventTimestamps_[2], thirdTimestamp);
}

TEST_F(
EventQueueCoalescingTest,
uniqueEventsWithDifferentCoalescingKeysDoNotCoalesce) {
auto eventQueue = createEventQueue();
auto firstTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(1);
auto secondTimestamp = HighResTimeStamp::fromDOMHighResTimeStamp(2);

eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 3, firstTimestamp));
eventQueue->enqueueUniqueEvent(
createRawEvent("topChange", 4, secondTimestamp));

eventQueue->flush(*runtime_);

ASSERT_EQ(eventTypes_.size(), 2);
EXPECT_EQ(eventTypes_[0], "topChange");
EXPECT_EQ(eventTypes_[1], "topChange");
EXPECT_EQ(eventTimestamps_[0], firstTimestamp);
EXPECT_EQ(eventTimestamps_[1], secondTimestamp);
}

} // namespace facebook::react
7 changes: 6 additions & 1 deletion scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1159,8 +1159,10 @@ class facebook::react::EventEmitter {
public void dispatchUniqueEvent(std::string type, const facebook::react::ValueFactory& payloadFactory, facebook::react::HighResTimeStamp eventTimestamp) const;
public void dispatchUniqueEvent(std::string type, facebook::react::SharedEventPayload payload) const;
public void dispatchUniqueEvent(std::string type, facebook::react::SharedEventPayload payload, facebook::react::HighResTimeStamp eventTimestamp) const;
public void dispatchUniqueEvent(std::string type, facebook::react::SharedEventPayload payload, facebook::react::HighResTimeStamp eventTimestamp, facebook::react::RawEvent::CoalescingKey coalescingKey) const;
public void dispatchUniqueEvent(std::string type, folly::dynamic&& payload) const;
public void dispatchUniqueEvent(std::string type, folly::dynamic&& payload, facebook::react::HighResTimeStamp eventTimestamp) const;
public void dispatchUniqueEvent(std::string type, folly::dynamic&& payload, facebook::react::HighResTimeStamp eventTimestamp, facebook::react::RawEvent::CoalescingKey coalescingKey) const;
public void setEnabled(bool enabled);
public void setShadowNodeFamily(std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily);
}
Expand Down Expand Up @@ -3512,15 +3514,18 @@ struct facebook::react::RadialGradientSize::Dimensions {
}

struct facebook::react::RawEvent {
public RawEvent(std::string type, facebook::react::SharedEventPayload eventPayload, facebook::react::SharedEventTarget eventTarget, std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily, facebook::react::RawEvent::Category category = facebook::react::RawEvent::Category::Unspecified, bool isUnique = false, facebook::react::HighResTimeStamp eventStartTimeStamp = facebook::react::HighResTimeStamp::now());
public RawEvent(std::string type, facebook::react::SharedEventPayload eventPayload, facebook::react::SharedEventTarget eventTarget, std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily, facebook::react::RawEvent::Category category = facebook::react::RawEvent::Category::Unspecified, bool isUnique = false, facebook::react::HighResTimeStamp eventStartTimeStamp = facebook::react::HighResTimeStamp::now(), facebook::react::RawEvent::CoalescingKey coalescingKey = facebook::react::RawEvent::DefaultCoalescingKey);
public bool isUnique;
public facebook::react::EventTag loggingTag;
public facebook::react::HighResTimeStamp eventStartTimeStamp;
public facebook::react::RawEvent::Category category;
public facebook::react::RawEvent::CoalescingKey coalescingKey;
public facebook::react::SharedEventPayload eventPayload;
public facebook::react::SharedEventTarget eventTarget;
public static constexpr facebook::react::RawEvent::CoalescingKey DefaultCoalescingKey;
public std::string type;
public std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily;
public using CoalescingKey = int;
}

enum facebook::react::RawEvent::Category {
Expand Down
7 changes: 6 additions & 1 deletion scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1866,8 +1866,10 @@ class facebook::react::EventEmitter {
public void dispatchUniqueEvent(std::string type, const facebook::react::ValueFactory& payloadFactory, facebook::react::HighResTimeStamp eventTimestamp) const;
public void dispatchUniqueEvent(std::string type, facebook::react::SharedEventPayload payload) const;
public void dispatchUniqueEvent(std::string type, facebook::react::SharedEventPayload payload, facebook::react::HighResTimeStamp eventTimestamp) const;
public void dispatchUniqueEvent(std::string type, facebook::react::SharedEventPayload payload, facebook::react::HighResTimeStamp eventTimestamp, facebook::react::RawEvent::CoalescingKey coalescingKey) const;
public void dispatchUniqueEvent(std::string type, folly::dynamic&& payload) const;
public void dispatchUniqueEvent(std::string type, folly::dynamic&& payload, facebook::react::HighResTimeStamp eventTimestamp) const;
public void dispatchUniqueEvent(std::string type, folly::dynamic&& payload, facebook::react::HighResTimeStamp eventTimestamp, facebook::react::RawEvent::CoalescingKey coalescingKey) const;
public void setEnabled(bool enabled);
public void setShadowNodeFamily(std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily);
}
Expand Down Expand Up @@ -6432,15 +6434,18 @@ struct facebook::react::RadialGradientSize::Dimensions {
}

struct facebook::react::RawEvent {
public RawEvent(std::string type, facebook::react::SharedEventPayload eventPayload, facebook::react::SharedEventTarget eventTarget, std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily, facebook::react::RawEvent::Category category = facebook::react::RawEvent::Category::Unspecified, bool isUnique = false, facebook::react::HighResTimeStamp eventStartTimeStamp = facebook::react::HighResTimeStamp::now());
public RawEvent(std::string type, facebook::react::SharedEventPayload eventPayload, facebook::react::SharedEventTarget eventTarget, std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily, facebook::react::RawEvent::Category category = facebook::react::RawEvent::Category::Unspecified, bool isUnique = false, facebook::react::HighResTimeStamp eventStartTimeStamp = facebook::react::HighResTimeStamp::now(), facebook::react::RawEvent::CoalescingKey coalescingKey = facebook::react::RawEvent::DefaultCoalescingKey);
public bool isUnique;
public facebook::react::EventTag loggingTag;
public facebook::react::HighResTimeStamp eventStartTimeStamp;
public facebook::react::RawEvent::Category category;
public facebook::react::RawEvent::CoalescingKey coalescingKey;
public facebook::react::SharedEventPayload eventPayload;
public facebook::react::SharedEventTarget eventTarget;
public static constexpr facebook::react::RawEvent::CoalescingKey DefaultCoalescingKey;
public std::string type;
public std::weak_ptr<const facebook::react::ShadowNodeFamily> shadowNodeFamily;
public using CoalescingKey = int;
}

enum facebook::react::RawEvent::Category {
Expand Down
Loading
Loading