From 7ce39fddfd5500dda1c849149bc01c146099942d Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Wed, 3 Mar 2021 09:13:46 -0800 Subject: [PATCH 1/8] Coalesce ScrollView Messages Fixes #7237 Fixes #7256 The overall goal of this change is to fix performance issues impacting FlatList. See the above issues for more details. This is largely based on what Android does, where they wait until the end of a frame, then queue a message to JS to pump the batch (with coalescing). This is exposed to the ABI, but is on the periphery instead of being added to ReactContext. --- .../CoalescingEventEmitter.cpp | 95 +++++++++++++++++++ .../CoalescingEventEmitter.h | 49 ++++++++++ .../CoalescingEventEmitter.idl | 25 +++++ .../Microsoft.ReactNative.vcxproj | 3 + .../Microsoft.ReactNative.vcxproj.filters | 5 +- .../ReactHost/ReactInstanceWin.cpp | 5 + .../Views/ScrollViewManager.cpp | 84 ++++++++++------ 7 files changed, 236 insertions(+), 30 deletions(-) create mode 100644 vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp create mode 100644 vnext/Microsoft.ReactNative/CoalescingEventEmitter.h create mode 100644 vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl diff --git a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp new file mode 100644 index 00000000000..c2d9faafd1c --- /dev/null +++ b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "CoalescingEventEmitter.h" +#include "CoalescingEventEmitter.g.cpp" +#include "DynamicWriter.h" + +namespace winrt::Microsoft::ReactNative::implementation { + +/*static*/ ReactPropertyId CoalescingEventEmitter::Property() noexcept { + static ReactPropertyId propId{L"ReactNative.CoalescingEventEmitter", L"Emitter"}; + return propId; +} + +/*static*/ winrt::Microsoft::ReactNative::CoalescingEventEmitter CoalescingEventEmitter::FromContext( + IReactContext const &context) noexcept { + return context.Properties() + .Get(CoalescingEventEmitter::Property().Handle()) + .as(); +} + +/*static*/ winrt::Microsoft::ReactNative::CoalescingEventEmitter CoalescingEventEmitter::FromContext( + Mso::React::IReactContext const &context) noexcept { + return context.Properties() + .Get(CoalescingEventEmitter::Property().Handle()) + .as(); +} + +CoalescingEventEmitter::CoalescingEventEmitter(Mso::CntPtr &&context) noexcept + : m_context(std::move(context)) {} + +void CoalescingEventEmitter::EmitJSEvent(int64_t coalesceKey, JSValueArgWriter const ¶msArgWriter) noexcept { + std::unique_lock lock(m_mutex); + + for (auto &evt : m_eventQueue) { + if (evt.coalesceKey && *evt.coalesceKey == coalesceKey) { + evt.expired = true; + } + } + + auto paramsWriter = winrt::make_self(); + paramsArgWriter(*paramsWriter); + + CoalescingEvent newEvent; + newEvent.coalesceKey = coalesceKey; + newEvent.args = paramsWriter->TakeValue(); + + m_eventQueue.push_back(std::move(newEvent)); + EnsureRenderCallbackRegistered(); +} + +void CoalescingEventEmitter::EmitDurableJSEvent(JSValueArgWriter const ¶msArgWriter) noexcept { + std::unique_lock lock(m_mutex); + + auto paramsWriter = winrt::make_self(); + paramsArgWriter(*paramsWriter); + + CoalescingEvent newEvent; + newEvent.args = paramsWriter->TakeValue(); + + m_eventQueue.push_back(std::move(newEvent)); + EnsureRenderCallbackRegistered(); +} + +void CoalescingEventEmitter::EnsureRenderCallbackRegistered() noexcept { + if (!m_renderingRevoker) { + m_renderingRevoker = xaml::Media::CompositionTarget::Rendering( + winrt::auto_revoke, [weakThis{get_weak()}](auto const &, auto const &) { + if (auto strongThis = weakThis.get()) { + strongThis->OnRendering(); + } + }); + } +} + +void CoalescingEventEmitter::OnRendering() noexcept { + std::unique_lock lock(m_mutex); + + // Submit the batch of requests to the JS queue + while (!m_eventQueue.empty()) { + auto &evt = m_eventQueue.front(); + if (!evt.expired) { + m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", std::move(evt.args)); + } + + m_eventQueue.pop_front(); + } + + // Don't leave the callback continuously registered as it can waste power. + // See https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.compositiontarget.rendering?view=winrt-19041 + m_renderingRevoker.revoke(); +} + +} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.h b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.h new file mode 100644 index 00000000000..139bdcf7a39 --- /dev/null +++ b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.h @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "CoalescingEventEmitter.g.h" +#include "JSValue.h" +#include "ReactHost/React.h" +#include "ReactPropertyBag.h" +#include "winrt/Microsoft.ReactNative.h" + +#include +#include + +namespace winrt::Microsoft::ReactNative::implementation { + +struct CoalescingEvent +{ + std::optional coalesceKey; + bool expired{false}; + folly::dynamic args; +}; + +struct CoalescingEventEmitter : CoalescingEventEmitterT { + public: + static ReactPropertyId Property() noexcept; + static winrt::Microsoft::ReactNative::CoalescingEventEmitter FromContext(IReactContext const & context) noexcept; + static winrt::Microsoft::ReactNative::CoalescingEventEmitter FromContext(Mso::React::IReactContext const & context) noexcept; + + CoalescingEventEmitter(Mso::CntPtr &&context) noexcept; + + void EmitJSEvent(int64_t coalesceKey, JSValueArgWriter const & paramsArgWriter) noexcept; + void EmitDurableJSEvent(JSValueArgWriter const & paramsArgWriter) noexcept; + + private: + void EnsureRenderCallbackRegistered() noexcept; + void OnRendering() noexcept; + + Mso::CntPtr m_context; + std::deque m_eventQueue; + std::mutex m_mutex; + xaml::Media::CompositionTarget::Rendering_revoker m_renderingRevoker; +}; + +} // namespace winrt::Microsoft::ReactNative::implementation + +namespace winrt::Microsoft::ReactNative::factory_implementation { +struct CoalescingEventEmitter : CoalescingEventEmitterT {}; +} // namespace winrt::Microsoft::ReactNative::factory_implementation diff --git a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl new file mode 100644 index 00000000000..03297ae7a9d --- /dev/null +++ b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import "IReactContext.idl"; + +#include "DocString.h" + +namespace Microsoft.ReactNative { + + [default_interface] + [webhosthidden] + DOC_STRING("Allows firing events delivered once-per-frame, optionally coalesced based on a supplied key") + runtimeclass CoalescingEventEmitter { + DOC_STRING("Allows access to a `CoalescingEventEmitter` from a `ReactContext`.") + static CoalescingEventEmitter FromContext(IReactContext context); + + DOC_STRING("Fires a coalescing event via RTCEventEmitter.receiveEvent." + "Accepts a `coalesceKey` to correlate events where only the latest of the key should be emitted.") + void EmitJSEvent(Int64 coalesceKey, JSValueArgWriter paramsArgWriter); + + DOC_STRING("Fires an event via RTCEventEmitter.receiveEvent. " + "Ordering is preserved relative to coalescing events, but the event is not itself coalesced.") + void EmitDurableJSEvent(JSValueArgWriter paramsArgWriter); + } +} // namespace Microsoft.ReactNative diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index 5c08461062c..daa9ea3161f 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -183,6 +183,7 @@ + DevMenuControl.xaml Code @@ -384,6 +385,7 @@ + DevMenuControl.xaml @@ -569,6 +571,7 @@ + DevMenuControl.xaml diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters index 69e88f3cc34..70b58d7e818 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters @@ -313,7 +313,7 @@ JSI - + @@ -668,7 +668,7 @@ - + @@ -706,6 +706,7 @@ + diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index 44d33e0f897..c002f9148ce 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -11,6 +11,7 @@ #include #include +#include "CoalescingEventEmitter.h" #include "ReactErrorProvider.h" #include "Microsoft.ReactNative/IReactNotificationService.h" @@ -626,6 +627,10 @@ void ReactInstanceWin::InitUIManager() noexcept { m_reactContext->Properties().Set( implementation::XamlUIService::XamlUIServiceProperty().Handle(), winrt::make(m_reactContext)); + + m_reactContext->Properties().Set( + implementation::CoalescingEventEmitter::Property().Handle(), + winrt::make(m_reactContext)); } facebook::react::NativeLoggingHook ReactInstanceWin::GetLoggingCallback() noexcept { diff --git a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp index 128160dfb76..89564c7fb52 100644 --- a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp @@ -3,6 +3,8 @@ #include "pch.h" +#include +#include #include #include #include @@ -10,8 +12,15 @@ #include "Impl/ScrollViewUWPImplementation.h" #include "ScrollViewManager.h" +using namespace winrt::Microsoft::ReactNative; + namespace Microsoft::ReactNative { +enum class CoalesceType { + CoalesceByTag, + Durable, +}; + namespace ScrollViewCommands { constexpr const char *ScrollTo = "scrollTo"; constexpr const char *ScrollToEnd = "scrollToEnd"; @@ -33,10 +42,11 @@ class ScrollViewShadowNode : public ShadowNodeBase { void EmitScrollEvent( const winrt::ScrollViewer &scrollViewer, int64_t tag, - const char *eventName, + const winrt::hstring &eventName, double x, double y, - double zoom); + double zoom, + CoalesceType coalesceType); template std::tuple getPropertyAndValidity( const winrt::Microsoft::ReactNative::JSValue &propertyValue, @@ -239,27 +249,30 @@ void ScrollViewShadowNode::AddHandlers(const winrt::ScrollViewer &scrollViewer) EmitScrollEvent( scrollViewerNotNull, m_tag, - "topScrollEndDrag", + L"topScrollEndDrag", args.NextView().HorizontalOffset(), args.NextView().VerticalOffset(), - args.NextView().ZoomFactor()); + args.NextView().ZoomFactor(), + CoalesceType::Durable); EmitScrollEvent( scrollViewerNotNull, m_tag, - "topScrollBeginMomentum", + L"topScrollBeginMomentum", args.NextView().HorizontalOffset(), args.NextView().VerticalOffset(), - args.NextView().ZoomFactor()); + args.NextView().ZoomFactor(), + CoalesceType::Durable); } EmitScrollEvent( scrollViewerNotNull, m_tag, - "topScroll", + L"topScroll", args.NextView().HorizontalOffset(), args.NextView().VerticalOffset(), - args.NextView().ZoomFactor()); + args.NextView().ZoomFactor(), + CoalesceType::CoalesceByTag); }); m_scrollViewerDirectManipulationStartedRevoker = @@ -274,10 +287,11 @@ void ScrollViewShadowNode::AddHandlers(const winrt::ScrollViewer &scrollViewer) EmitScrollEvent( scrollViewer, m_tag, - "topScrollBeginDrag", + L"topScrollBeginDrag", scrollViewer.HorizontalOffset(), scrollViewer.VerticalOffset(), - scrollViewer.ZoomFactor()); + scrollViewer.ZoomFactor(), + CoalesceType::Durable); }); m_scrollViewerDirectManipulationCompletedRevoker = @@ -287,18 +301,20 @@ void ScrollViewShadowNode::AddHandlers(const winrt::ScrollViewer &scrollViewer) EmitScrollEvent( scrollViewer, m_tag, - "topScrollEndMomentum", + L"topScrollEndMomentum", scrollViewer.HorizontalOffset(), scrollViewer.VerticalOffset(), - scrollViewer.ZoomFactor()); + scrollViewer.ZoomFactor(), + CoalesceType::Durable); } else { EmitScrollEvent( scrollViewer, m_tag, - "topScrollEndDrag", + L"topScrollEndDrag", scrollViewer.HorizontalOffset(), scrollViewer.VerticalOffset(), - scrollViewer.ZoomFactor()); + scrollViewer.ZoomFactor(), + CoalesceType::Durable); } m_isScrolling = false; @@ -316,29 +332,41 @@ void ScrollViewShadowNode::AddHandlers(const winrt::ScrollViewer &scrollViewer) void ScrollViewShadowNode::EmitScrollEvent( const winrt::ScrollViewer &scrollViewer, int64_t tag, - const char *eventName, + const winrt::hstring &eventName, double x, double y, - double zoom) { + double zoom, + CoalesceType coalesceType) { const auto scrollViewerNotNull = scrollViewer; - folly::dynamic offset = folly::dynamic::object("x", x)("y", y); + JSValueObject contentOffset{{"x", x}, {"y", y}}; + JSValueObject contentInset{{"left", 0}, {"top", 0}, {"right", 0}, {"bottom", 0}}; - folly::dynamic contentInset = folly::dynamic::object("left", 0)("top", 0)("right", 0)("bottom", 0); + JSValueObject contentSize{ + {"width", scrollViewerNotNull.ExtentWidth()}, {"height", scrollViewerNotNull.ExtentHeight()}}; - folly::dynamic contentSize = - folly::dynamic::object("width", scrollViewerNotNull.ExtentWidth())("height", scrollViewerNotNull.ExtentHeight()); + JSValueObject layoutMeasurement{ + {"width", scrollViewerNotNull.ActualWidth()}, {"height", scrollViewerNotNull.ActualHeight()}}; - folly::dynamic layoutSize = - folly::dynamic::object("width", scrollViewerNotNull.ActualWidth())("height", scrollViewerNotNull.ActualHeight()); + JSValueObject eventJson{ + {"target", tag}, + {"responderIgnoreScroll", true}, + {"contentOffset", std::move(contentOffset)}, + {"contentInset", std::move(contentInset)}, + {"contentSize", std::move(contentSize)}, + {"layoutMeasurement", std::move(layoutMeasurement)}, + {"zoomScale", zoom}}; - folly::dynamic eventJson = - folly::dynamic::object("target", tag)("responderIgnoreScroll", true)("contentOffset", offset)( - "contentInset", contentInset)("contentSize", contentSize)("layoutMeasurement", layoutSize)("zoomScale", zoom); + JSValueArray params{tag, winrt::to_string(eventName), std::move(eventJson)}; - folly::dynamic params = folly::dynamic::array(tag, eventName, eventJson); - GetViewManager()->GetReactContext().CallJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); -} + auto coalescingEmitter = implementation::CoalescingEventEmitter::FromContext(GetViewManager()->GetReactContext()); + + if (coalesceType == CoalesceType::CoalesceByTag) { + coalescingEmitter.EmitJSEvent(tag, [&](const IJSValueWriter &writer) noexcept { params.WriteTo(writer); }); + } else { + coalescingEmitter.EmitDurableJSEvent([&](const IJSValueWriter &writer) noexcept { params.WriteTo(writer); }); + } +} // namespace Microsoft::ReactNative template std::tuple ScrollViewShadowNode::getPropertyAndValidity( From da0f2ed9239d65af757410c3175a70c1cac8f37b Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Wed, 3 Mar 2021 09:14:11 -0800 Subject: [PATCH 2/8] Change files --- ...ative-windows-2602b5c0-d5d5-4250-a951-5a26601422b2.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-2602b5c0-d5d5-4250-a951-5a26601422b2.json diff --git a/change/react-native-windows-2602b5c0-d5d5-4250-a951-5a26601422b2.json b/change/react-native-windows-2602b5c0-d5d5-4250-a951-5a26601422b2.json new file mode 100644 index 00000000000..e0286c51502 --- /dev/null +++ b/change/react-native-windows-2602b5c0-d5d5-4250-a951-5a26601422b2.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Coalesce ScrollView Messages", + "packageName": "react-native-windows", + "email": "ngerlem@microsoft.com", + "dependentChangeType": "patch" +} From 6ab8b73ae627ed6fe81cb5aea5bb89d8cdac458f Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Wed, 3 Mar 2021 10:18:55 -0800 Subject: [PATCH 3/8] whoops --- .../CoalescingEventEmitter.cpp | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp index c2d9faafd1c..3755562445f 100644 --- a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp +++ b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp @@ -75,17 +75,24 @@ void CoalescingEventEmitter::EnsureRenderCallbackRegistered() noexcept { } void CoalescingEventEmitter::OnRendering() noexcept { - std::unique_lock lock(m_mutex); + auto jsDispatcher = m_context->Properties().Get(ReactDispatcherHelper::JSDispatcherProperty()).as(); // Submit the batch of requests to the JS queue - while (!m_eventQueue.empty()) { - auto &evt = m_eventQueue.front(); - if (!evt.expired) { - m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", std::move(evt.args)); + jsDispatcher.Post([weakThis{get_weak()}]() noexcept { + auto strongThis = weakThis.get(); + if (!strongThis) { + return; } - m_eventQueue.pop_front(); - } + while (!strongThis->m_eventQueue.empty()) { + auto &evt = strongThis->m_eventQueue.front(); + if (!evt.expired) { + strongThis->m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", std::move(evt.args)); + } + + strongThis->m_eventQueue.pop_front(); + } + }); // Don't leave the callback continuously registered as it can waste power. // See https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.compositiontarget.rendering?view=winrt-19041 From 65eb45d7095ed2ac659a073d1d72ca8659eb546f Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 4 Mar 2021 03:27:11 -0800 Subject: [PATCH 4/8] - Remove from ABI, so we can iterate on interface privately - Move to emitter-per-viewmanager instead of emitter-per-instance. This helps with removing conflicts and keeping short queue length - Remove coalesceKey in favor of automatically partitioning based on event name + tag - Make friendlier to C++ non/ABI generated types - Lots of renaming and documentation to make intent clearer - A couple of internal changes --- .../CoalescingEventEmitter.cpp | 102 ------------------ .../CoalescingEventEmitter.h | 49 --------- .../CoalescingEventEmitter.idl | 25 ----- .../Microsoft.ReactNative.vcxproj | 5 +- .../Microsoft.ReactNative.vcxproj.filters | 9 +- .../ReactHost/ReactInstanceWin.cpp | 5 - .../Utils/BatchingEventEmitter.cpp | 84 +++++++++++++++ .../Utils/BatchingEventEmitter.h | 49 +++++++++ .../Views/ScrollViewManager.cpp | 21 ++-- .../Views/ScrollViewManager.h | 5 + 10 files changed, 158 insertions(+), 196 deletions(-) delete mode 100644 vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp delete mode 100644 vnext/Microsoft.ReactNative/CoalescingEventEmitter.h delete mode 100644 vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl create mode 100644 vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp create mode 100644 vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h diff --git a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp deleted file mode 100644 index 3755562445f..00000000000 --- a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.cpp +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#include "pch.h" -#include "CoalescingEventEmitter.h" -#include "CoalescingEventEmitter.g.cpp" -#include "DynamicWriter.h" - -namespace winrt::Microsoft::ReactNative::implementation { - -/*static*/ ReactPropertyId CoalescingEventEmitter::Property() noexcept { - static ReactPropertyId propId{L"ReactNative.CoalescingEventEmitter", L"Emitter"}; - return propId; -} - -/*static*/ winrt::Microsoft::ReactNative::CoalescingEventEmitter CoalescingEventEmitter::FromContext( - IReactContext const &context) noexcept { - return context.Properties() - .Get(CoalescingEventEmitter::Property().Handle()) - .as(); -} - -/*static*/ winrt::Microsoft::ReactNative::CoalescingEventEmitter CoalescingEventEmitter::FromContext( - Mso::React::IReactContext const &context) noexcept { - return context.Properties() - .Get(CoalescingEventEmitter::Property().Handle()) - .as(); -} - -CoalescingEventEmitter::CoalescingEventEmitter(Mso::CntPtr &&context) noexcept - : m_context(std::move(context)) {} - -void CoalescingEventEmitter::EmitJSEvent(int64_t coalesceKey, JSValueArgWriter const ¶msArgWriter) noexcept { - std::unique_lock lock(m_mutex); - - for (auto &evt : m_eventQueue) { - if (evt.coalesceKey && *evt.coalesceKey == coalesceKey) { - evt.expired = true; - } - } - - auto paramsWriter = winrt::make_self(); - paramsArgWriter(*paramsWriter); - - CoalescingEvent newEvent; - newEvent.coalesceKey = coalesceKey; - newEvent.args = paramsWriter->TakeValue(); - - m_eventQueue.push_back(std::move(newEvent)); - EnsureRenderCallbackRegistered(); -} - -void CoalescingEventEmitter::EmitDurableJSEvent(JSValueArgWriter const ¶msArgWriter) noexcept { - std::unique_lock lock(m_mutex); - - auto paramsWriter = winrt::make_self(); - paramsArgWriter(*paramsWriter); - - CoalescingEvent newEvent; - newEvent.args = paramsWriter->TakeValue(); - - m_eventQueue.push_back(std::move(newEvent)); - EnsureRenderCallbackRegistered(); -} - -void CoalescingEventEmitter::EnsureRenderCallbackRegistered() noexcept { - if (!m_renderingRevoker) { - m_renderingRevoker = xaml::Media::CompositionTarget::Rendering( - winrt::auto_revoke, [weakThis{get_weak()}](auto const &, auto const &) { - if (auto strongThis = weakThis.get()) { - strongThis->OnRendering(); - } - }); - } -} - -void CoalescingEventEmitter::OnRendering() noexcept { - auto jsDispatcher = m_context->Properties().Get(ReactDispatcherHelper::JSDispatcherProperty()).as(); - - // Submit the batch of requests to the JS queue - jsDispatcher.Post([weakThis{get_weak()}]() noexcept { - auto strongThis = weakThis.get(); - if (!strongThis) { - return; - } - - while (!strongThis->m_eventQueue.empty()) { - auto &evt = strongThis->m_eventQueue.front(); - if (!evt.expired) { - strongThis->m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", std::move(evt.args)); - } - - strongThis->m_eventQueue.pop_front(); - } - }); - - // Don't leave the callback continuously registered as it can waste power. - // See https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.compositiontarget.rendering?view=winrt-19041 - m_renderingRevoker.revoke(); -} - -} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.h b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.h deleted file mode 100644 index 139bdcf7a39..00000000000 --- a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.h +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#pragma once - -#include "CoalescingEventEmitter.g.h" -#include "JSValue.h" -#include "ReactHost/React.h" -#include "ReactPropertyBag.h" -#include "winrt/Microsoft.ReactNative.h" - -#include -#include - -namespace winrt::Microsoft::ReactNative::implementation { - -struct CoalescingEvent -{ - std::optional coalesceKey; - bool expired{false}; - folly::dynamic args; -}; - -struct CoalescingEventEmitter : CoalescingEventEmitterT { - public: - static ReactPropertyId Property() noexcept; - static winrt::Microsoft::ReactNative::CoalescingEventEmitter FromContext(IReactContext const & context) noexcept; - static winrt::Microsoft::ReactNative::CoalescingEventEmitter FromContext(Mso::React::IReactContext const & context) noexcept; - - CoalescingEventEmitter(Mso::CntPtr &&context) noexcept; - - void EmitJSEvent(int64_t coalesceKey, JSValueArgWriter const & paramsArgWriter) noexcept; - void EmitDurableJSEvent(JSValueArgWriter const & paramsArgWriter) noexcept; - - private: - void EnsureRenderCallbackRegistered() noexcept; - void OnRendering() noexcept; - - Mso::CntPtr m_context; - std::deque m_eventQueue; - std::mutex m_mutex; - xaml::Media::CompositionTarget::Rendering_revoker m_renderingRevoker; -}; - -} // namespace winrt::Microsoft::ReactNative::implementation - -namespace winrt::Microsoft::ReactNative::factory_implementation { -struct CoalescingEventEmitter : CoalescingEventEmitterT {}; -} // namespace winrt::Microsoft::ReactNative::factory_implementation diff --git a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl b/vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl deleted file mode 100644 index 03297ae7a9d..00000000000 --- a/vnext/Microsoft.ReactNative/CoalescingEventEmitter.idl +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import "IReactContext.idl"; - -#include "DocString.h" - -namespace Microsoft.ReactNative { - - [default_interface] - [webhosthidden] - DOC_STRING("Allows firing events delivered once-per-frame, optionally coalesced based on a supplied key") - runtimeclass CoalescingEventEmitter { - DOC_STRING("Allows access to a `CoalescingEventEmitter` from a `ReactContext`.") - static CoalescingEventEmitter FromContext(IReactContext context); - - DOC_STRING("Fires a coalescing event via RTCEventEmitter.receiveEvent." - "Accepts a `coalesceKey` to correlate events where only the latest of the key should be emitted.") - void EmitJSEvent(Int64 coalesceKey, JSValueArgWriter paramsArgWriter); - - DOC_STRING("Fires an event via RTCEventEmitter.receiveEvent. " - "Ordering is preserved relative to coalescing events, but the event is not itself coalesced.") - void EmitDurableJSEvent(JSValueArgWriter paramsArgWriter); - } -} // namespace Microsoft.ReactNative diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index daa9ea3161f..1a3aa60d91d 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -183,7 +183,7 @@ - + DevMenuControl.xaml Code @@ -385,7 +385,7 @@ - + DevMenuControl.xaml @@ -571,7 +571,6 @@ - DevMenuControl.xaml diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters index 70b58d7e818..c77761c624a 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters @@ -313,7 +313,9 @@ JSI - + + Utils + @@ -668,7 +670,9 @@ - + + Utils + @@ -706,7 +710,6 @@ - diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index c002f9148ce..44d33e0f897 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -11,7 +11,6 @@ #include #include -#include "CoalescingEventEmitter.h" #include "ReactErrorProvider.h" #include "Microsoft.ReactNative/IReactNotificationService.h" @@ -627,10 +626,6 @@ void ReactInstanceWin::InitUIManager() noexcept { m_reactContext->Properties().Set( implementation::XamlUIService::XamlUIServiceProperty().Handle(), winrt::make(m_reactContext)); - - m_reactContext->Properties().Set( - implementation::CoalescingEventEmitter::Property().Handle(), - winrt::make(m_reactContext)); } facebook::react::NativeLoggingHook ReactInstanceWin::GetLoggingCallback() noexcept { diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp new file mode 100644 index 00000000000..d241060c3c6 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "BatchingEventEmitter.h" +#include "DynamicWriter.h" +#include "JSValueWriter.h" + +namespace winrt::Microsoft::ReactNative { + +BatchingEventEmitter::BatchingEventEmitter(Mso::CntPtr &&context) noexcept + : m_context(std::move(context)) {} + +void BatchingEventEmitter::EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept { + implementation::BatchedEvent newEvent{tag, std::move(eventName), std::move(eventObject)}; + + { + std::unique_lock lock(m_eventQueueMutex); + + m_eventQueue.push_back(std::move(newEvent)); + + // Once a frame is presented, send a task to JS to emit all elements in the + // queue. We know the task is pending on the UI thread or JS thread if we + // have a non-zero size queue, so we want to only trigger pumping when the + // first element is added to an empty queue. + if (m_eventQueue.size() == 1) { + m_renderingRevoker = xaml::Media::CompositionTarget::Rendering( + winrt::auto_revoke, [weakThis{get_weak()}](auto const &, auto const &) { + if (auto strongThis = weakThis.get()) { + strongThis->OnRendering(); + } + }); + } + } +} + +void BatchingEventEmitter::EmitCoalescingJSEvent( + int64_t tag, + winrt::hstring &&eventName, + JSValueObject &&eventObject) noexcept { + { + std::unique_lock lock(m_eventQueueMutex); + auto endIter = std::remove_if(m_eventQueue.begin(), m_eventQueue.end(), [&](const auto &evt) noexcept { + return evt.eventName == eventName && evt.tag == tag; + }); + + m_eventQueue.erase(endIter, m_eventQueue.end()); + } + + EmitJSEvent(tag, std::move(eventName), std::move(eventObject)); +} + +void BatchingEventEmitter::OnRendering() noexcept { + auto jsDispatcher = m_context->Properties().Get(ReactDispatcherHelper::JSDispatcherProperty()).as(); + + jsDispatcher.Post([weakThis{get_weak()}]() noexcept { + auto strongThis = weakThis.get(); + if (!strongThis) { + return; + } + + std::unique_lock lock(strongThis->m_eventQueueMutex); + + while (!strongThis->m_eventQueue.empty()) { + auto &evt = strongThis->m_eventQueue.front(); + + auto paramsWriter = winrt::make_self(); + paramsWriter->WriteArrayBegin(); + WriteValue(*paramsWriter, evt.tag); + WriteValue(*paramsWriter, evt.eventName); + WriteValue(*paramsWriter, evt.eventObject); + paramsWriter->WriteArrayEnd(); + + strongThis->m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", paramsWriter->TakeValue()); + strongThis->m_eventQueue.pop_front(); + } + }); + + // Don't leave the callback continuously registered as it can waste power. + // See https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.compositiontarget.rendering?view=winrt-19041 + m_renderingRevoker.revoke(); +} + +} // namespace winrt::Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h new file mode 100644 index 00000000000..eee7e1b1fff --- /dev/null +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "JSValue.h" +#include "ReactHost/React.h" +#include "ReactPropertyBag.h" +#include "winrt/Microsoft.ReactNative.h" + +#include +#include + +namespace winrt::Microsoft::ReactNative::implementation { +struct BatchedEvent { + int64_t tag{}; + winrt::hstring eventName; + JSValueObject eventObject; +}; +} // namespace winrt::Microsoft::ReactNative::implementation + +namespace winrt::Microsoft::ReactNative { + +//! Emits queued events from native to JS in queued batches (at most once per-native frame). Events within a batch may +//! be coalesced. The batch is finished at the time the JS thread starts to process it. I.e. it is possible for a batch +//! to last for multiple frames if the JS thread is blocked. This is by-design as it allows our coalescing strategy to +//! account for long operations on the JS thread. +struct BatchingEventEmitter : winrt::implements { + public: + BatchingEventEmitter(Mso::CntPtr &&context) noexcept; + + //! Queues an event to be fired via RTCEventEmitter.receiveEvent(). + void EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept; + + //! Queues an event to be fired via RTCEventEmitter.receiveEvent(). Existing events in the batch with the same name + //! and tag will be removed. + void EmitCoalescingJSEvent(int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept; + + private: + void EnsureQueuePumping() noexcept; + void OnRendering() noexcept; + + Mso::CntPtr m_context; + std::deque m_eventQueue; + std::mutex m_eventQueueMutex; + xaml::Media::CompositionTarget::Rendering_revoker m_renderingRevoker; +}; + +} // namespace winrt::Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp index 89564c7fb52..1681f91c50d 100644 --- a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp @@ -3,7 +3,6 @@ #include "pch.h" -#include #include #include #include @@ -42,7 +41,7 @@ class ScrollViewShadowNode : public ShadowNodeBase { void EmitScrollEvent( const winrt::ScrollViewer &scrollViewer, int64_t tag, - const winrt::hstring &eventName, + winrt::hstring &&eventName, double x, double y, double zoom, @@ -332,7 +331,7 @@ void ScrollViewShadowNode::AddHandlers(const winrt::ScrollViewer &scrollViewer) void ScrollViewShadowNode::EmitScrollEvent( const winrt::ScrollViewer &scrollViewer, int64_t tag, - const winrt::hstring &eventName, + winrt::hstring &&eventName, double x, double y, double zoom, @@ -357,14 +356,12 @@ void ScrollViewShadowNode::EmitScrollEvent( {"layoutMeasurement", std::move(layoutMeasurement)}, {"zoomScale", zoom}}; - JSValueArray params{tag, winrt::to_string(eventName), std::move(eventJson)}; - - auto coalescingEmitter = implementation::CoalescingEventEmitter::FromContext(GetViewManager()->GetReactContext()); + auto *viewManager = static_cast(GetViewManager()); if (coalesceType == CoalesceType::CoalesceByTag) { - coalescingEmitter.EmitJSEvent(tag, [&](const IJSValueWriter &writer) noexcept { params.WriteTo(writer); }); + viewManager->BatchingEmitter().EmitCoalescingJSEvent(tag, std::move(eventName), std::move(eventJson)); } else { - coalescingEmitter.EmitDurableJSEvent([&](const IJSValueWriter &writer) noexcept { params.WriteTo(writer); }); + viewManager->BatchingEmitter().EmitJSEvent(tag, std::move(eventName), std::move(eventJson)); } } // namespace Microsoft::ReactNative @@ -419,7 +416,9 @@ void ScrollViewShadowNode::UpdateZoomMode(const winrt::ScrollViewer &scrollViewe : winrt::ZoomMode::Disabled); } -ScrollViewManager::ScrollViewManager(const Mso::React::IReactContext &context) : Super(context) {} +ScrollViewManager::ScrollViewManager(const Mso::React::IReactContext &context) : Super(context) { + m_batchingEventEmitter = winrt::make_self(Mso::CntPtr(&context)); +} const wchar_t *ScrollViewManager::GetName() const { return L"RCTScrollView"; @@ -537,4 +536,8 @@ void ScrollViewManager::SnapToOffsets(const XamlView &parent, const winrt::IVect } } +BatchingEventEmitter &ScrollViewManager::BatchingEmitter() noexcept { + return *m_batchingEventEmitter; +} + } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h index e2b4f181d27..1b3368caac3 100644 --- a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h +++ b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "Impl/ScrollViewUWPImplementation.h" @@ -29,11 +30,15 @@ class ScrollViewManager : public ControlViewManager { void SnapToInterval(const XamlView &parent, float interval); void SnapToOffsets(const XamlView &parent, const winrt::IVectorView &offsets); + winrt::Microsoft::ReactNative::BatchingEventEmitter &BatchingEmitter() noexcept; + protected: XamlView CreateViewCore(int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &) override; private: friend class ScrollViewShadowNode; + + winrt::com_ptr m_batchingEventEmitter; }; } // namespace Microsoft::ReactNative From a349471658295320a78209a1037eb78397d4b226 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 4 Mar 2021 04:01:06 -0800 Subject: [PATCH 5/8] More fixes --- .../Utils/BatchingEventEmitter.cpp | 56 +++++++++++-------- .../Utils/BatchingEventEmitter.h | 5 +- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp index d241060c3c6..345a4a91145 100644 --- a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp @@ -9,9 +9,13 @@ namespace winrt::Microsoft::ReactNative { BatchingEventEmitter::BatchingEventEmitter(Mso::CntPtr &&context) noexcept - : m_context(std::move(context)) {} + : m_context(std::move(context)) { + m_uiDispatcher = m_context->Properties().Get(ReactDispatcherHelper::UIDispatcherProperty()).as(); +} void BatchingEventEmitter::EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept { + VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); + implementation::BatchedEvent newEvent{tag, std::move(eventName), std::move(eventObject)}; { @@ -27,7 +31,7 @@ void BatchingEventEmitter::EmitJSEvent(int64_t tag, winrt::hstring &&eventName, m_renderingRevoker = xaml::Media::CompositionTarget::Rendering( winrt::auto_revoke, [weakThis{get_weak()}](auto const &, auto const &) { if (auto strongThis = weakThis.get()) { - strongThis->OnRendering(); + strongThis->OnFrameUI(); } }); } @@ -38,6 +42,8 @@ void BatchingEventEmitter::EmitCoalescingJSEvent( int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept { + VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); + { std::unique_lock lock(m_eventQueueMutex); auto endIter = std::remove_if(m_eventQueue.begin(), m_eventQueue.end(), [&](const auto &evt) noexcept { @@ -50,29 +56,12 @@ void BatchingEventEmitter::EmitCoalescingJSEvent( EmitJSEvent(tag, std::move(eventName), std::move(eventObject)); } -void BatchingEventEmitter::OnRendering() noexcept { +void BatchingEventEmitter::OnFrameUI() noexcept { auto jsDispatcher = m_context->Properties().Get(ReactDispatcherHelper::JSDispatcherProperty()).as(); jsDispatcher.Post([weakThis{get_weak()}]() noexcept { - auto strongThis = weakThis.get(); - if (!strongThis) { - return; - } - - std::unique_lock lock(strongThis->m_eventQueueMutex); - - while (!strongThis->m_eventQueue.empty()) { - auto &evt = strongThis->m_eventQueue.front(); - - auto paramsWriter = winrt::make_self(); - paramsWriter->WriteArrayBegin(); - WriteValue(*paramsWriter, evt.tag); - WriteValue(*paramsWriter, evt.eventName); - WriteValue(*paramsWriter, evt.eventObject); - paramsWriter->WriteArrayEnd(); - - strongThis->m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", paramsWriter->TakeValue()); - strongThis->m_eventQueue.pop_front(); + if (auto strongThis = weakThis.get()) { + strongThis->OnFrameJS(); } }); @@ -81,4 +70,27 @@ void BatchingEventEmitter::OnRendering() noexcept { m_renderingRevoker.revoke(); } +void BatchingEventEmitter::OnFrameJS() noexcept { + std::deque currentBatch; + + { + std::unique_lock lock(m_eventQueueMutex); + currentBatch.swap(m_eventQueue); + } + + while (!currentBatch.empty()) { + auto &evt = currentBatch.front(); + + auto paramsWriter = winrt::make_self(); + paramsWriter->WriteArrayBegin(); + WriteValue(*paramsWriter, evt.tag); + WriteValue(*paramsWriter, evt.eventName); + WriteValue(*paramsWriter, evt.eventObject); + paramsWriter->WriteArrayEnd(); + + m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", paramsWriter->TakeValue()); + currentBatch.pop_front(); + } +} + } // namespace winrt::Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h index eee7e1b1fff..4da5d3beb63 100644 --- a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h @@ -37,13 +37,14 @@ struct BatchingEventEmitter : winrt::implements m_context; std::deque m_eventQueue; std::mutex m_eventQueueMutex; xaml::Media::CompositionTarget::Rendering_revoker m_renderingRevoker; + IReactDispatcher m_uiDispatcher; }; } // namespace winrt::Microsoft::ReactNative From 3c052fb5ac2aeceafdbab24facb34934eb4191e3 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 4 Mar 2021 12:23:49 -0800 Subject: [PATCH 6/8] PR feedback --- .../Utils/BatchingEventEmitter.cpp | 63 ++++++++++++------- .../Utils/BatchingEventEmitter.h | 11 ++-- .../Views/ScrollViewManager.cpp | 5 +- .../Views/ScrollViewManager.h | 2 +- 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp index 345a4a91145..4626bbb1cbf 100644 --- a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.cpp @@ -13,39 +13,58 @@ BatchingEventEmitter::BatchingEventEmitter(Mso::CntPtrProperties().Get(ReactDispatcherHelper::UIDispatcherProperty()).as(); } -void BatchingEventEmitter::EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept { +void BatchingEventEmitter::EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept { + return EmitJSEvent(L"receiveEvent", tag, std::move(eventName), std::move(eventObject)); +} + +void BatchingEventEmitter::EmitJSEvent( + winrt::hstring &&emitterMethod, + int64_t tag, + winrt::hstring &&eventName, + JSValue &&eventObject) noexcept { VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); - implementation::BatchedEvent newEvent{tag, std::move(eventName), std::move(eventObject)}; + implementation::BatchedEvent newEvent{std::move(emitterMethod), tag, std::move(eventName), std::move(eventObject)}; + + size_t queueSizeAfterInsert; { - std::unique_lock lock(m_eventQueueMutex); + std::scoped_lock lock(m_eventQueueMutex); m_eventQueue.push_back(std::move(newEvent)); + queueSizeAfterInsert = m_eventQueue.size(); + } - // Once a frame is presented, send a task to JS to emit all elements in the - // queue. We know the task is pending on the UI thread or JS thread if we - // have a non-zero size queue, so we want to only trigger pumping when the - // first element is added to an empty queue. - if (m_eventQueue.size() == 1) { - m_renderingRevoker = xaml::Media::CompositionTarget::Rendering( - winrt::auto_revoke, [weakThis{get_weak()}](auto const &, auto const &) { - if (auto strongThis = weakThis.get()) { - strongThis->OnFrameUI(); - } - }); - } + // Once a frame is presented, send a task to JS to emit all elements in the + // queue. We know the task is pending on the UI thread or JS thread if we + // have a non-zero size queue, so we want to only trigger pumping when the + // first element is added to an empty queue. + if (queueSizeAfterInsert == 1) { + m_renderingRevoker = xaml::Media::CompositionTarget::Rendering( + winrt::auto_revoke, [weakThis{weak_from_this()}](auto const &, auto const &) { + if (auto strongThis = weakThis.lock()) { + strongThis->OnFrameUI(); + } + }); } } void BatchingEventEmitter::EmitCoalescingJSEvent( int64_t tag, winrt::hstring &&eventName, - JSValueObject &&eventObject) noexcept { + JSValue &&eventObject) noexcept { + return EmitCoalescingJSEvent(L"receiveEvent", tag, std::move(eventName), std::move(eventObject)); +} + +void BatchingEventEmitter::EmitCoalescingJSEvent( + winrt::hstring &&emitterMethod, + int64_t tag, + winrt::hstring &&eventName, + JSValue &&eventObject) noexcept { VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); { - std::unique_lock lock(m_eventQueueMutex); + std::scoped_lock lock(m_eventQueueMutex); auto endIter = std::remove_if(m_eventQueue.begin(), m_eventQueue.end(), [&](const auto &evt) noexcept { return evt.eventName == eventName && evt.tag == tag; }); @@ -53,14 +72,14 @@ void BatchingEventEmitter::EmitCoalescingJSEvent( m_eventQueue.erase(endIter, m_eventQueue.end()); } - EmitJSEvent(tag, std::move(eventName), std::move(eventObject)); + EmitJSEvent(std::move(emitterMethod), tag, std::move(eventName), std::move(eventObject)); } void BatchingEventEmitter::OnFrameUI() noexcept { auto jsDispatcher = m_context->Properties().Get(ReactDispatcherHelper::JSDispatcherProperty()).as(); - jsDispatcher.Post([weakThis{get_weak()}]() noexcept { - if (auto strongThis = weakThis.get()) { + jsDispatcher.Post([weakThis{weak_from_this()}]() noexcept { + if (auto strongThis = weakThis.lock()) { strongThis->OnFrameJS(); } }); @@ -74,7 +93,7 @@ void BatchingEventEmitter::OnFrameJS() noexcept { std::deque currentBatch; { - std::unique_lock lock(m_eventQueueMutex); + std::scoped_lock lock(m_eventQueueMutex); currentBatch.swap(m_eventQueue); } @@ -88,7 +107,7 @@ void BatchingEventEmitter::OnFrameJS() noexcept { WriteValue(*paramsWriter, evt.eventObject); paramsWriter->WriteArrayEnd(); - m_context->CallJSFunction("RCTEventEmitter", "receiveEvent", paramsWriter->TakeValue()); + m_context->CallJSFunction("RCTEventEmitter", winrt::to_string(evt.emitterMethod), paramsWriter->TakeValue()); currentBatch.pop_front(); } } diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h index 4da5d3beb63..2d50df0e771 100644 --- a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h @@ -13,9 +13,10 @@ namespace winrt::Microsoft::ReactNative::implementation { struct BatchedEvent { + winrt::hstring emitterMethod; int64_t tag{}; winrt::hstring eventName; - JSValueObject eventObject; + JSValue eventObject; }; } // namespace winrt::Microsoft::ReactNative::implementation @@ -25,16 +26,18 @@ namespace winrt::Microsoft::ReactNative { //! be coalesced. The batch is finished at the time the JS thread starts to process it. I.e. it is possible for a batch //! to last for multiple frames if the JS thread is blocked. This is by-design as it allows our coalescing strategy to //! account for long operations on the JS thread. -struct BatchingEventEmitter : winrt::implements { +struct BatchingEventEmitter : public std::enable_shared_from_this { public: BatchingEventEmitter(Mso::CntPtr &&context) noexcept; //! Queues an event to be fired via RTCEventEmitter.receiveEvent(). - void EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept; + void EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept; + void EmitJSEvent(winrt::hstring &&emitterMethod, int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept; //! Queues an event to be fired via RTCEventEmitter.receiveEvent(). Existing events in the batch with the same name //! and tag will be removed. - void EmitCoalescingJSEvent(int64_t tag, winrt::hstring &&eventName, JSValueObject &&eventObject) noexcept; + void EmitCoalescingJSEvent(int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept; + void EmitCoalescingJSEvent(winrt::hstring &&emitterMethod, int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept; private: void OnFrameUI() noexcept; diff --git a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp index 1681f91c50d..ce4db3e9145 100644 --- a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.cpp @@ -416,9 +416,8 @@ void ScrollViewShadowNode::UpdateZoomMode(const winrt::ScrollViewer &scrollViewe : winrt::ZoomMode::Disabled); } -ScrollViewManager::ScrollViewManager(const Mso::React::IReactContext &context) : Super(context) { - m_batchingEventEmitter = winrt::make_self(Mso::CntPtr(&context)); -} +ScrollViewManager::ScrollViewManager(const Mso::React::IReactContext &context) + : Super(context), m_batchingEventEmitter{std::make_shared(Mso::CntPtr(&context))} {} const wchar_t *ScrollViewManager::GetName() const { return L"RCTScrollView"; diff --git a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h index 1b3368caac3..4c1697b22fb 100644 --- a/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h +++ b/vnext/Microsoft.ReactNative/Views/ScrollViewManager.h @@ -38,7 +38,7 @@ class ScrollViewManager : public ControlViewManager { private: friend class ScrollViewShadowNode; - winrt::com_ptr m_batchingEventEmitter; + std::shared_ptr m_batchingEventEmitter; }; } // namespace Microsoft::ReactNative From 4a7523b73e0b3327e02e473979dbb4dd20eb66cf Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 4 Mar 2021 12:36:55 -0800 Subject: [PATCH 7/8] yarn format --- vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h index 2d50df0e771..098b2603c86 100644 --- a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h @@ -32,12 +32,17 @@ struct BatchingEventEmitter : public std::enable_shared_from_this Date: Thu, 4 Mar 2021 12:43:13 -0800 Subject: [PATCH 8/8] Fix comment typos --- .../Utils/BatchingEventEmitter.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h index 098b2603c86..3d4a1863462 100644 --- a/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h +++ b/vnext/Microsoft.ReactNative/Utils/BatchingEventEmitter.h @@ -22,21 +22,21 @@ struct BatchedEvent { namespace winrt::Microsoft::ReactNative { -//! Emits queued events from native to JS in queued batches (at most once per-native frame). Events within a batch may -//! be coalesced. The batch is finished at the time the JS thread starts to process it. I.e. it is possible for a batch -//! to last for multiple frames if the JS thread is blocked. This is by-design as it allows our coalescing strategy to +//! Emits events from native to JS in queued batches (at most once per-native frame). Events within a batch may be +//! coalesced. The batch is finished at the time the JS thread starts to process it. I.e. it is possible for a batch to +//! last for multiple frames if the JS thread is blocked. This is by-design as it allows our coalescing strategy to //! account for long operations on the JS thread. struct BatchingEventEmitter : public std::enable_shared_from_this { public: BatchingEventEmitter(Mso::CntPtr &&context) noexcept; - //! Queues an event to be fired via RTCEventEmitter.receiveEvent(). + //! Queues an event to be fired via RCTEventEmitter, calling receiveEvent() by default. void EmitJSEvent(int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept; void EmitJSEvent(winrt::hstring &&emitterMethod, int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept; - //! Queues an event to be fired via RTCEventEmitter.receiveEvent(). Existing events in the batch with the same name - //! and tag will be removed. + //! Queues an event to be fired via RCTEventEmitter, calling receiveEvent() by default. Existing events in the batch + //! with the same name and tag will be removed. void EmitCoalescingJSEvent(int64_t tag, winrt::hstring &&eventName, JSValue &&eventObject) noexcept; void EmitCoalescingJSEvent( winrt::hstring &&emitterMethod,