diff --git a/packages/react-native/React/React-RCTFabric.podspec b/packages/react-native/React/React-RCTFabric.podspec index 7cfb18a8dd1d53..a2fd54a87118b0 100644 --- a/packages/react-native/React/React-RCTFabric.podspec +++ b/packages/react-native/React/React-RCTFabric.podspec @@ -86,6 +86,7 @@ Pod::Spec.new do |s| add_dependency(s, "React-featureflags") add_dependency(s, "React-debug") add_dependency(s, "React-utils") + add_dependency(s, "React-performancetimeline") add_dependency(s, "React-rendererdebug") add_dependency(s, "React-rendererconsistency") add_dependency(s, "React-runtimescheduler") diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 46ebc34418b8d7..0f2465f770d026 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -557,6 +557,7 @@ android { "react_codegen_rncore", "react_debug", "react_featureflags", + "react_performance_timeline", "react_utils", "react_render_componentregistry", "react_newarchdefaults", @@ -565,6 +566,7 @@ android { "react_render_consistency", "react_render_dom", "react_render_graphics", + "react_render_observers_events", "rrc_image", "rrc_root", "rrc_view", diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 464503c4216c38..87347f9637aa11 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -71,6 +71,7 @@ add_react_common_subdir(react/renderer/runtimescheduler) add_react_common_subdir(react/debug) add_react_common_subdir(react/config) add_react_common_subdir(react/featureflags) +add_react_common_subdir(react/performance/timeline) add_react_common_subdir(react/renderer/animations) add_react_common_subdir(react/renderer/attributedstring) add_react_common_subdir(react/renderer/componentregistry) @@ -99,6 +100,7 @@ add_react_common_subdir(react/renderer/components/unimplementedview) add_react_common_subdir(react/renderer/components/modal) add_react_common_subdir(react/renderer/components/scrollview) add_react_common_subdir(react/renderer/leakchecker) +add_react_common_subdir(react/renderer/observers/events) add_react_common_subdir(react/renderer/textlayoutmanager) add_react_common_subdir(react/utils) add_react_common_subdir(react/bridging) diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index d7af3b162a30cb..69129f22abde9a 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -249,11 +249,24 @@ Pod::Spec.new do |s| ss.header_dir = "react/renderer/mounting" end + s.subspec "observers" do |ss| + ss.subspec "events" do |sss| + sss.dependency folly_dep_name, folly_version + sss.compiler_flags = folly_compiler_flags + sss.source_files = "react/renderer/observers/events/**/*.{m,mm,cpp,h}" + sss.exclude_files = "react/renderer/observers/events/tests" + sss.header_dir = "react/renderer/observers/events" + end + end + s.subspec "scheduler" do |ss| ss.dependency folly_dep_name, folly_version ss.compiler_flags = folly_compiler_flags ss.source_files = "react/renderer/scheduler/**/*.{m,mm,cpp,h}" ss.header_dir = "react/renderer/scheduler" + + ss.dependency "React-performancetimeline" + ss.dependency "React-Fabric/observers/events" end s.subspec "templateprocessor" do |ss| diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp index 2c4e45e4c28139..4f377122c15311 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp @@ -10,8 +10,8 @@ #include #include #include +#include #include "NativePerformance.h" -#include "PerformanceEntryReporter.h" #include "Plugins.h" @@ -34,7 +34,7 @@ void NativePerformance::mark( jsi::Runtime& rt, std::string name, double startTime) { - PerformanceEntryReporter::getInstance().mark(name, startTime); + PerformanceEntryReporter::getInstance()->mark(name, startTime); } void NativePerformance::measure( @@ -45,7 +45,7 @@ void NativePerformance::measure( std::optional duration, std::optional startMark, std::optional endMark) { - PerformanceEntryReporter::getInstance().measure( + PerformanceEntryReporter::getInstance()->measure( name, startTime, endTime, duration, startMark, endMark); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h index 65c1ef07e02fab..1c95d80abd4118 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h @@ -11,14 +11,7 @@ #include #include -#include "NativePerformanceObserver.h" - namespace facebook::react { -class PerformanceEntryReporter; - -#pragma mark - Structs - -#pragma mark - implementation class NativePerformance : public NativePerformanceCxxSpec { public: @@ -54,8 +47,6 @@ class NativePerformance : public NativePerformanceCxxSpec { // tracking. std::unordered_map getReactNativeStartupTiming( jsi::Runtime& rt); - - private: }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp index 5f4a5bf3264cc9..b84c961c2821b2 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp @@ -8,8 +8,8 @@ #include #include "NativePerformanceObserver.h" -#include "PerformanceEntryReporter.h" +#include #include #include @@ -26,114 +26,97 @@ namespace facebook::react { NativePerformanceObserver::NativePerformanceObserver( std::shared_ptr jsInvoker) - : NativePerformanceObserverCxxSpec(std::move(jsInvoker)) { - setEventLogger(&PerformanceEntryReporter::getInstance()); -} - -NativePerformanceObserver::~NativePerformanceObserver() { - setEventLogger(nullptr); -} + : NativePerformanceObserverCxxSpec(std::move(jsInvoker)) {} void NativePerformanceObserver::startReporting( - jsi::Runtime& rt, - int32_t entryType) { - PerformanceEntryReporter& reporter = PerformanceEntryReporter::getInstance(); - - PerformanceEntryType entryTypeEnum = - static_cast(entryType); - reporter.startReporting(entryTypeEnum); - - if (entryTypeEnum == PerformanceEntryType::EVENT && - CoreFeatures::enableReportEventPaintTime) { - UIManagerBinding::getBinding(rt)->getUIManager().registerMountHook( - reporter); - } + jsi::Runtime& /*rt*/, + PerformanceEntryType entryType) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->startReporting(entryType); } void NativePerformanceObserver::stopReporting( - jsi::Runtime& rt, - int32_t entryType) { - PerformanceEntryReporter& reporter = PerformanceEntryReporter::getInstance(); - - PerformanceEntryType entryTypeEnum = - static_cast(entryType); - reporter.stopReporting(entryTypeEnum); - - if (entryTypeEnum == PerformanceEntryType::EVENT && - CoreFeatures::enableReportEventPaintTime) { - UIManagerBinding::getBinding(rt)->getUIManager().unregisterMountHook( - reporter); - } + jsi::Runtime& /*rt*/, + PerformanceEntryType entryType) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->stopReporting(entryType); } void NativePerformanceObserver::setIsBuffered( - jsi::Runtime& rt, - std::vector entryTypes, + jsi::Runtime& /*rt*/, + const std::vector entryTypes, bool isBuffered) { - for (const int32_t entryType : entryTypes) { - PerformanceEntryReporter::getInstance().setAlwaysLogged( - static_cast(entryType), isBuffered); + for (const PerformanceEntryType entryType : entryTypes) { + PerformanceEntryReporter::getInstance()->setAlwaysLogged( + entryType, isBuffered); } } -GetPendingEntriesResult NativePerformanceObserver::popPendingEntries( - jsi::Runtime& rt) { - return PerformanceEntryReporter::getInstance().popPendingEntries(); +PerformanceEntryReporter::PopPendingEntriesResult +NativePerformanceObserver::popPendingEntries(jsi::Runtime& /*rt*/) { + return PerformanceEntryReporter::getInstance()->popPendingEntries(); } void NativePerformanceObserver::setOnPerformanceEntryCallback( - jsi::Runtime& rt, + jsi::Runtime& /*rt*/, std::optional> callback) { - PerformanceEntryReporter::getInstance().setReportingCallback(callback); + if (callback) { + PerformanceEntryReporter::getInstance()->setReportingCallback( + [callback = std::move(callback)]() { + callback->callWithPriority(SchedulerPriority::IdlePriority); + }); + } else { + PerformanceEntryReporter::getInstance()->setReportingCallback(nullptr); + } } void NativePerformanceObserver::logRawEntry( - jsi::Runtime& rt, - RawPerformanceEntry entry) { - PerformanceEntryReporter::getInstance().logEntry(entry); + jsi::Runtime& /*rt*/, + const PerformanceEntry entry) { + PerformanceEntryReporter::getInstance()->logEntry(entry); } std::vector> -NativePerformanceObserver::getEventCounts(jsi::Runtime& rt) { +NativePerformanceObserver::getEventCounts(jsi::Runtime& /*rt*/) { const auto& eventCounts = - PerformanceEntryReporter::getInstance().getEventCounts(); + PerformanceEntryReporter::getInstance()->getEventCounts(); return std::vector>( eventCounts.begin(), eventCounts.end()); } void NativePerformanceObserver::setDurationThreshold( - jsi::Runtime& rt, - int32_t entryType, + jsi::Runtime& /*rt*/, + PerformanceEntryType entryType, double durationThreshold) { - PerformanceEntryReporter::getInstance().setDurationThreshold( - static_cast(entryType), durationThreshold); + PerformanceEntryReporter::getInstance()->setDurationThreshold( + entryType, durationThreshold); } void NativePerformanceObserver::clearEntries( - jsi::Runtime& rt, - int32_t entryType, + jsi::Runtime& /*rt*/, + PerformanceEntryType entryType, std::optional entryName) { - PerformanceEntryReporter::getInstance().clearEntries( - static_cast(entryType), - entryName ? entryName->c_str() : nullptr); + PerformanceEntryReporter::getInstance()->clearEntries( + entryType, entryName ? entryName->c_str() : std::string_view{}); } -std::vector NativePerformanceObserver::getEntries( - jsi::Runtime& rt, - std::optional entryType, +std::vector NativePerformanceObserver::getEntries( + jsi::Runtime& /*rt*/, + std::optional entryType, std::optional entryName) { - return PerformanceEntryReporter::getInstance().getEntries( - entryType ? std::optional{static_cast(*entryType)} - : std::nullopt, - entryName ? entryName->c_str() : nullptr); + return PerformanceEntryReporter::getInstance()->getEntries( + entryType, entryName ? entryName->c_str() : std::string_view{}); } -std::vector -NativePerformanceObserver::getSupportedPerformanceEntryTypes(jsi::Runtime& rt) { +std::vector +NativePerformanceObserver::getSupportedPerformanceEntryTypes( + jsi::Runtime& /*rt*/) { return { - static_cast(PerformanceEntryType::MARK), - static_cast(PerformanceEntryType::MEASURE), - static_cast(PerformanceEntryType::EVENT), + PerformanceEntryType::MARK, + PerformanceEntryType::MEASURE, + PerformanceEntryType::EVENT, }; } diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h index 8271bde666ef11..e2e4e3245716b6 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h @@ -8,43 +8,40 @@ #pragma once #include +#include #include #include #include #include namespace facebook::react { -class PerformanceEntryReporter; #pragma mark - Structs -using RawPerformanceEntryType = int32_t; - -using RawPerformanceEntry = NativePerformanceObserverCxxRawPerformanceEntry< - /* name */ std::string, - /* type */ RawPerformanceEntryType, - /* startTime */ double, - /* duration */ double, - - // For "event" entries only: - /* processingStart */ std::optional, - /* processingEnd */ std::optional, - /* interactionId */ std::optional>; +template <> +struct Bridging { + static PerformanceEntryType fromJs( + jsi::Runtime& /*rt*/, + const jsi::Value& value) { + return static_cast(value.asNumber()); + } + + static jsi::Value toJs( + jsi::Runtime& /*rt*/, + const PerformanceEntryType& value) { + return {static_cast(value)}; + } +}; template <> -struct Bridging +struct Bridging : NativePerformanceObserverCxxRawPerformanceEntryBridging< - RawPerformanceEntry> {}; - -using GetPendingEntriesResult = - NativePerformanceObserverCxxGetPendingEntriesResult< - std::vector, - uint32_t>; + PerformanceEntry> {}; template <> -struct Bridging +struct Bridging : NativePerformanceObserverCxxGetPendingEntriesResultBridging< - GetPendingEntriesResult> {}; + PerformanceEntryReporter::PopPendingEntriesResult> {}; #pragma mark - implementation @@ -52,47 +49,45 @@ class NativePerformanceObserver : public NativePerformanceObserverCxxSpec { public: NativePerformanceObserver(std::shared_ptr jsInvoker); - ~NativePerformanceObserver(); - void startReporting(jsi::Runtime& rt, int32_t entryType); + void startReporting(jsi::Runtime& rt, PerformanceEntryType entryType); - void stopReporting(jsi::Runtime& rt, int32_t entryType); + void stopReporting(jsi::Runtime& rt, PerformanceEntryType entryType); void setIsBuffered( jsi::Runtime& rt, - std::vector entryTypes, + const std::vector entryTypes, bool isBuffered); - GetPendingEntriesResult popPendingEntries(jsi::Runtime& rt); + PerformanceEntryReporter::PopPendingEntriesResult popPendingEntries( + jsi::Runtime& rt); void setOnPerformanceEntryCallback( jsi::Runtime& rt, std::optional> callback); - void logRawEntry(jsi::Runtime& rt, RawPerformanceEntry entry); + void logRawEntry(jsi::Runtime& rt, const PerformanceEntry entry); std::vector> getEventCounts( jsi::Runtime& rt); void setDurationThreshold( jsi::Runtime& rt, - int32_t entryType, - double durationThreshold); + PerformanceEntryType entryType, + DOMHighResTimeStamp durationThreshold); void clearEntries( jsi::Runtime& rt, - int32_t entryType, + PerformanceEntryType entryType, std::optional entryName); - std::vector getEntries( + std::vector getEntries( jsi::Runtime& rt, - std::optional entryType, + std::optional entryType, std::optional entryName); - std::vector getSupportedPerformanceEntryTypes( + std::vector getSupportedPerformanceEntryTypes( jsi::Runtime& rt); - - private: }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/PerformanceEntryReporter.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/PerformanceEntryReporter.cpp deleted file mode 100644 index 743ca4b5aecaa8..00000000000000 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/PerformanceEntryReporter.cpp +++ /dev/null @@ -1,468 +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. - */ - -#include "PerformanceEntryReporter.h" -#include -#include -#include -#include "NativePerformanceObserver.h" - -#include -#include - -#include - -namespace facebook::react { -EventTag PerformanceEntryReporter::sCurrentEventTag_{0}; - -PerformanceEntryReporter& PerformanceEntryReporter::getInstance() { - static PerformanceEntryReporter instance; - return instance; -} - -PerformanceEntryReporter::PerformanceEntryReporter() { - // For mark entry types we also want to keep the lookup by name, to make - // sure that marks can be referenced by measures - getBuffer(PerformanceEntryType::MARK).hasNameLookup = true; -} - -void PerformanceEntryReporter::setReportingCallback( - std::optional> callback) { - callback_ = callback; -} - -double PerformanceEntryReporter::getCurrentTimeStamp() const { - return timeStampProvider_ != nullptr ? timeStampProvider_() - : JSExecutor::performanceNow(); -} - -void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) { - auto& buffer = getBuffer(entryType); - buffer.isReporting = true; - buffer.durationThreshold = DEFAULT_DURATION_THRESHOLD; -} - -void PerformanceEntryReporter::setAlwaysLogged( - PerformanceEntryType entryType, - bool isAlwaysLogged) { - auto& buffer = getBuffer(entryType); - buffer.isAlwaysLogged = isAlwaysLogged; -} - -void PerformanceEntryReporter::setDurationThreshold( - PerformanceEntryType entryType, - double durationThreshold) { - getBuffer(entryType).durationThreshold = durationThreshold; -} - -void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) { - getBuffer(entryType).isReporting = false; -} - -void PerformanceEntryReporter::stopReporting() { - for (auto& buffer : buffers_) { - buffer.isReporting = false; - } -} - -GetPendingEntriesResult PerformanceEntryReporter::popPendingEntries() { - std::lock_guard lock(entriesMutex_); - GetPendingEntriesResult res = { - std::vector(), droppedEntryCount_}; - for (auto& buffer : buffers_) { - buffer.entries.consume(res.entries); - } - - // Sort by starting time (or ending time, if starting times are equal) - std::stable_sort( - res.entries.begin(), - res.entries.end(), - [](const RawPerformanceEntry& lhs, const RawPerformanceEntry& rhs) { - if (lhs.startTime != rhs.startTime) { - return lhs.startTime < rhs.startTime; - } else { - return lhs.duration < rhs.duration; - } - }); - - droppedEntryCount_ = 0; - return res; -} - -void PerformanceEntryReporter::logEntry(const RawPerformanceEntry& entry) { - const auto entryType = static_cast(entry.entryType); - if (entryType == PerformanceEntryType::EVENT) { - eventCounts_[entry.name]++; - } - - if (!isReporting(entryType) && !isAlwaysLogged(entryType)) { - return; - } - - std::lock_guard lock(entriesMutex_); - - auto& buffer = getBuffer(entryType); - - if (entry.duration < buffer.durationThreshold) { - // The entries duration is lower than the desired reporting threshold, skip - return; - } - - if (buffer.hasNameLookup) { - // If we need to remove an entry because the buffer is null, - // we also need to remove it from the name lookup. - auto overwriteCandidate = buffer.entries.getNextOverwriteCandidate(); - if (overwriteCandidate != nullptr) { - std::lock_guard lock2(nameLookupMutex_); - auto it = buffer.nameLookup.find(overwriteCandidate); - if (it != buffer.nameLookup.end() && *it == overwriteCandidate) { - buffer.nameLookup.erase(it); - } - } - } - - auto pushResult = buffer.entries.add(std::move(entry)); - if (pushResult == - BoundedConsumableBuffer::PushStatus::DROP) { - // Start dropping entries once reached maximum buffer size. - // The number of dropped entries will be reported back to the corresponding - // PerformanceObserver callback. - droppedEntryCount_ += 1; - } - - if (buffer.hasNameLookup) { - std::lock_guard lock2(nameLookupMutex_); - auto currentEntry = &buffer.entries.back(); - auto it = buffer.nameLookup.find(currentEntry); - if (it != buffer.nameLookup.end()) { - buffer.nameLookup.erase(it); - } - buffer.nameLookup.insert(currentEntry); - } - - if (buffer.entries.getNumToConsume() == 1) { - // If the buffer was empty, it signals that JS side just has possibly - // consumed it and is ready to get more - scheduleFlushBuffer(); - } -} - -void PerformanceEntryReporter::mark( - const std::string& name, - const std::optional& startTime) { - logEntry(RawPerformanceEntry{ - .name = name, - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = startTime ? *startTime : getCurrentTimeStamp()}); -} - -void PerformanceEntryReporter::clearEntries( - std::optional entryType, - std::string_view entryName) { - if (!entryType) { - // Clear all entry types - for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { - clearEntries(static_cast(i), entryName); - } - } else { - auto& buffer = getBuffer(*entryType); - if (!entryName.empty()) { - if (buffer.hasNameLookup) { - std::lock_guard lock2(nameLookupMutex_); - buffer.nameLookup.clear(); - } - - std::lock_guard lock(entriesMutex_); - buffer.entries.clear([entryName](const RawPerformanceEntry& entry) { - return entry.name == entryName; - }); - - if (buffer.hasNameLookup) { - std::lock_guard lock2(nameLookupMutex_); - // BoundedConsumableBuffer::clear() invalidates existing references; we - // need to rebuild the lookup table. If there are multiple entries with - // the same name, make sure the last one gets inserted. - for (int i = static_cast(buffer.entries.size()) - 1; i >= 0; i--) { - const auto& entry = buffer.entries[i]; - buffer.nameLookup.insert(&entry); - } - } - } else { - { - std::lock_guard lock(entriesMutex_); - buffer.entries.clear(); - } - { - std::lock_guard lock2(nameLookupMutex_); - buffer.nameLookup.clear(); - } - } - } -} - -void PerformanceEntryReporter::getEntries( - PerformanceEntryType entryType, - std::string_view entryName, - std::vector& res) const { - std::lock_guard lock(entriesMutex_); - const auto& entries = getBuffer(entryType).entries; - if (entryName.empty()) { - entries.getEntries(res); - } else { - entries.getEntries(res, [entryName](const RawPerformanceEntry& entry) { - return entry.name == entryName; - }); - } -} - -std::vector PerformanceEntryReporter::getEntries( - std::optional entryType, - std::string_view entryName) const { - std::vector res; - if (!entryType) { - // Collect all entry types - for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { - getEntries(static_cast(i), entryName, res); - } - } else { - getEntries(*entryType, entryName, res); - } - return res; -} - -void PerformanceEntryReporter::measure( - const std::string& name, - double startTime, - double endTime, - const std::optional& duration, - const std::optional& startMark, - const std::optional& endMark) { - double startTimeVal = startMark ? getMarkTime(*startMark) : startTime; - double endTimeVal = endMark ? getMarkTime(*endMark) : endTime; - - if (!endMark && endTime < startTimeVal) { - // The end time is not specified, take the current time, according to the - // standard - endTimeVal = getCurrentTimeStamp(); - } - - double durationVal = duration ? *duration : endTimeVal - startTimeVal; - - logEntry( - {.name = name, - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = startTimeVal, - .duration = durationVal}); -} - -double PerformanceEntryReporter::getMarkTime( - const std::string& markName) const { - RawPerformanceEntry mark{ - .name = markName, - .entryType = static_cast(PerformanceEntryType::MARK)}; - - std::lock_guard lock(nameLookupMutex_); - const auto& marksBuffer = getBuffer(PerformanceEntryType::MARK); - auto it = marksBuffer.nameLookup.find(&mark); - if (it != marksBuffer.nameLookup.end()) { - return (*it)->startTime; - } else { - return 0.0; - } -} - -void PerformanceEntryReporter::logEventEntry( - std::string name, - double startTime, - double duration, - double processingStart, - double processingEnd, - uint32_t interactionId) { - logEntry( - {.name = std::move(name), - .entryType = static_cast(PerformanceEntryType::EVENT), - .startTime = startTime, - .duration = duration, - .processingStart = processingStart, - .processingEnd = processingEnd, - .interactionId = interactionId}); -} - -void PerformanceEntryReporter::scheduleFlushBuffer() { - if (callback_) { - callback_->callWithPriority(SchedulerPriority::IdlePriority); - } -} - -struct StrKey { - uint32_t key; - StrKey(std::string_view s) : key(std::hash{}(s)) {} - - bool operator==(const StrKey& rhs) const { - return key == rhs.key; - } -}; - -struct StrKeyHash { - constexpr size_t operator()(const StrKey& strKey) const { - return static_cast(strKey.key); - } -}; - -// Supported events for reporting, see -// https://www.w3.org/TR/event-timing/#sec-events-exposed -// Not all of these are currently supported by RN, but we map them anyway for -// future-proofing. -using SupportedEventTypeRegistry = - std::unordered_map; - -static const SupportedEventTypeRegistry& getSupportedEvents() { - static SupportedEventTypeRegistry SUPPORTED_EVENTS = { - {StrKey("topAuxClick"), "auxclick"}, - {StrKey("topClick"), "click"}, - {StrKey("topContextMenu"), "contextmenu"}, - {StrKey("topDblClick"), "dblclick"}, - {StrKey("topMouseDown"), "mousedown"}, - {StrKey("topMouseEnter"), "mouseenter"}, - {StrKey("topMouseLeave"), "mouseleave"}, - {StrKey("topMouseOut"), "mouseout"}, - {StrKey("topMouseOver"), "mouseover"}, - {StrKey("topMouseUp"), "mouseup"}, - {StrKey("topPointerOver"), "pointerover"}, - {StrKey("topPointerEnter"), "pointerenter"}, - {StrKey("topPointerDown"), "pointerdown"}, - {StrKey("topPointerUp"), "pointerup"}, - {StrKey("topPointerCancel"), "pointercancel"}, - {StrKey("topPointerOut"), "pointerout"}, - {StrKey("topPointerLeave"), "pointerleave"}, - {StrKey("topGotPointerCapture"), "gotpointercapture"}, - {StrKey("topLostPointerCapture"), "lostpointercapture"}, - {StrKey("topTouchStart"), "touchstart"}, - {StrKey("topTouchEnd"), "touchend"}, - {StrKey("topTouchCancel"), "touchcancel"}, - {StrKey("topKeyDown"), "keydown"}, - {StrKey("topKeyPress"), "keypress"}, - {StrKey("topKeyUp"), "keyup"}, - {StrKey("topBeforeInput"), "beforeinput"}, - {StrKey("topInput"), "input"}, - {StrKey("topCompositionStart"), "compositionstart"}, - {StrKey("topCompositionUpdate"), "compositionupdate"}, - {StrKey("topCompositionEnd"), "compositionend"}, - {StrKey("topDragStart"), "dragstart"}, - {StrKey("topDragEnd"), "dragend"}, - {StrKey("topDragEnter"), "dragenter"}, - {StrKey("topDragLeave"), "dragleave"}, - {StrKey("topDragOver"), "dragover"}, - {StrKey("topDrop"), "drop"}, - }; - return SUPPORTED_EVENTS; -} - -EventTag PerformanceEntryReporter::onEventStart(std::string_view name) { - if (!isReporting(PerformanceEntryType::EVENT)) { - return 0; - } - const auto& supportedEvents = getSupportedEvents(); - auto it = supportedEvents.find(name); - if (it == supportedEvents.end()) { - return 0; - } - - auto reportedName = it->second; - - sCurrentEventTag_++; - if (sCurrentEventTag_ == 0) { - // The tag wrapped around (which is highly unlikely, but still) - sCurrentEventTag_ = 1; - } - - auto timeStamp = getCurrentTimeStamp(); - { - std::lock_guard lock(eventsInFlightMutex_); - eventsInFlight_.emplace(std::make_pair( - sCurrentEventTag_, EventEntry{reportedName, timeStamp, 0.0})); - } - return sCurrentEventTag_; -} - -void PerformanceEntryReporter::onEventProcessingStart(EventTag tag) { - if (!isReporting(PerformanceEntryType::EVENT) || tag == 0) { - return; - } - auto timeStamp = getCurrentTimeStamp(); - { - std::lock_guard lock(eventsInFlightMutex_); - auto it = eventsInFlight_.find(tag); - if (it != eventsInFlight_.end()) { - it->second.processingStartTime = timeStamp; - } - } -} - -void PerformanceEntryReporter::onEventProcessingEnd(EventTag tag) { - if (!isReporting(PerformanceEntryType::EVENT) || tag == 0) { - return; - } - auto timeStamp = getCurrentTimeStamp(); - { - std::lock_guard lock(eventsInFlightMutex_); - auto it = eventsInFlight_.find(tag); - if (it == eventsInFlight_.end()) { - return; - } - auto& entry = it->second; - entry.processingEndTime = timeStamp; - - if (CoreFeatures::enableReportEventPaintTime) { - // If reporting paint time, don't send the entry just yet and wait for the - // mount hook callback to be called - return; - } - - const auto& name = entry.name; - - logEventEntry( - std::string(name), - entry.startTime, - timeStamp - entry.startTime, - entry.processingStartTime, - entry.processingEndTime, - entry.interactionId); - eventsInFlight_.erase(it); - } -} - -void PerformanceEntryReporter::shadowTreeDidMount( - const RootShadowNode::Shared& /*rootShadowNode*/, - double mountTime) noexcept { - if (!isReporting(PerformanceEntryType::EVENT) || - !CoreFeatures::enableReportEventPaintTime) { - return; - } - - std::lock_guard lock(eventsInFlightMutex_); - auto it = eventsInFlight_.begin(); - while (it != eventsInFlight_.end()) { - const auto& entry = it->second; - if (entry.processingEndTime == 0.0 || entry.processingEndTime > mountTime) { - // This mount doesn't correspond to the event - ++it; - continue; - } - - logEventEntry( - std::string(entry.name), - entry.startTime, - mountTime - entry.startTime, - entry.processingStartTime, - entry.processingEndTime, - entry.interactionId); - it = eventsInFlight_.erase(it); - } -} - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/tests/PerformanceEntryReporterTest.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/tests/PerformanceEntryReporterTest.cpp deleted file mode 100644 index b1b0e50be5164b..00000000000000 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/tests/PerformanceEntryReporterTest.cpp +++ /dev/null @@ -1,334 +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. - */ - -#include - -#include - -#include "../PerformanceEntryReporter.h" - -namespace facebook::react { - -static std::ostream& operator<<( - std::ostream& os, - const RawPerformanceEntry& entry) { - static constexpr const char* entryTypeNames[] = { - "UNDEFINED", - "MARK", - "MEASURE", - "EVENT", - }; - return os << "{ name: " << entry.name - << ", type: " << entryTypeNames[entry.entryType] - << ", startTime: " << entry.startTime - << ", duration: " << entry.duration << " }"; -} -} // namespace facebook::react - -using namespace facebook::react; - -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStartReporting) { - auto& reporter = PerformanceEntryReporter::getInstance(); - - reporter.stopReporting(); - reporter.clearEntries(); - - reporter.startReporting(PerformanceEntryType::MARK); - reporter.startReporting(PerformanceEntryType::MEASURE); - - ASSERT_TRUE(reporter.isReporting(PerformanceEntryType::MARK)); - ASSERT_TRUE(reporter.isReporting(PerformanceEntryType::MEASURE)); - - ASSERT_FALSE(reporter.isReporting(PerformanceEntryType::EVENT)); -} - -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStopReporting) { - auto& reporter = PerformanceEntryReporter::getInstance(); - - reporter.stopReporting(); - reporter.clearEntries(); - - reporter.startReporting(PerformanceEntryType::MARK); - - reporter.mark("mark0", 0.0); - reporter.mark("mark1", 0.0); - reporter.mark("mark2", 0.0); - reporter.measure("measure0", 0.0, 0.0); - - auto res = reporter.popPendingEntries(); - const auto& entries = res.entries; - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(3, entries.size()); - - res = reporter.popPendingEntries(); - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(0, res.entries.size()); - - reporter.stopReporting(PerformanceEntryType::MARK); - reporter.startReporting(PerformanceEntryType::MEASURE); - - reporter.mark("mark3"); - reporter.measure("measure1", 0.0, 0.0); - - res = reporter.popPendingEntries(); - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(1, res.entries.size()); - ASSERT_STREQ("measure1", res.entries[0].name.c_str()); -} - -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMarks) { - auto& reporter = PerformanceEntryReporter::getInstance(); - - reporter.stopReporting(); - reporter.clearEntries(); - - reporter.startReporting(PerformanceEntryType::MARK); - - reporter.mark("mark0", 0.0); - reporter.mark("mark1", 1.0); - reporter.mark("mark2", 2.0); - // Report mark0 again - reporter.mark("mark0", 3.0); - - auto res = reporter.popPendingEntries(); - const auto& entries = res.entries; - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(4, entries.size()); - - const std::vector expected = { - {.name = "mark0", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 0.0}, - {.name = "mark1", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 1.0}, - {.name = "mark2", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 2.0}, - {.name = "mark0", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 3.0}, - }; - - ASSERT_EQ(expected, entries); -} - -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { - auto& reporter = PerformanceEntryReporter::getInstance(); - - reporter.stopReporting(); - reporter.clearEntries(); - - reporter.startReporting(PerformanceEntryType::MARK); - reporter.startReporting(PerformanceEntryType::MEASURE); - - reporter.mark("mark0", 0.0); - reporter.mark("mark1", 1.0); - reporter.mark("mark2", 2.0); - - reporter.measure("measure0", 0.0, 2.0); - reporter.measure("measure1", 0.0, 2.0, 4.0); - reporter.measure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2"); - reporter.measure("measure3", 0.0, 0.0, 5.0, "mark1"); - reporter.measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); - - reporter.setTimeStampProvider([]() { return 3.5; }); - reporter.measure("measure5", 0.0, 0.0, std::nullopt, "mark2"); - - reporter.mark("mark3", 2.0); - reporter.measure("measure6", 2.0, 2.0); - reporter.mark("mark4", 2.0); - reporter.mark("mark4", 3.0); - // Uses the last reported time for mark4 - reporter.measure("measure7", 0.0, 0.0, std::nullopt, "mark1", "mark4"); - - auto res = reporter.popPendingEntries(); - const auto& entries = res.entries; - - ASSERT_EQ(0, res.droppedEntriesCount); - - const std::vector expected = { - {.name = "mark0", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 0.0}, - {.name = "measure0", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 0.0, - .duration = 2.0}, - {.name = "measure1", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 0.0, - .duration = 4.0}, - {.name = "mark1", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 1.0}, - {.name = "measure2", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 1.0, - .duration = 1.0}, - {.name = "measure7", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 1.0, - .duration = 2.0}, - {.name = "measure3", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 1.0, - .duration = 5.0}, - {.name = "measure4", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 1.5, - .duration = 0.5}, - {.name = "mark2", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 2.0}, - {.name = "mark3", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 2.0}, - {.name = "mark4", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 2.0}, - {.name = "measure6", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 2.0, - .duration = 0.0}, - {.name = "measure5", - .entryType = static_cast(PerformanceEntryType::MEASURE), - .startTime = 2.0, - .duration = 1.5}, - {.name = "mark4", - .entryType = static_cast(PerformanceEntryType::MARK), - .startTime = 3.0}}; - - ASSERT_EQ(expected, entries); -} - -static std::vector getNames( - const std::vector& entries) { - std::vector res; - std::transform( - entries.begin(), - entries.end(), - std::back_inserter(res), - [](const RawPerformanceEntry& e) { return e.name; }); - return res; -} - -static std::vector getTypes( - const std::vector& entries) { - std::vector res; - std::transform( - entries.begin(), - entries.end(), - std::back_inserter(res), - [](const RawPerformanceEntry& e) { return e.entryType; }); - return res; -} - -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) { - auto& reporter = PerformanceEntryReporter::getInstance(); - - reporter.stopReporting(); - reporter.clearEntries(); - - auto res = reporter.popPendingEntries(); - const auto& entries = res.entries; - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(0, entries.size()); - - reporter.startReporting(PerformanceEntryType::MARK); - reporter.startReporting(PerformanceEntryType::MEASURE); - - reporter.mark("common_name", 0.0); - reporter.mark("mark1", 1.0); - reporter.mark("mark2", 2.0); - - reporter.measure("common_name", 0.0, 2.0); - reporter.measure("measure1", 0.0, 2.0, 4.0); - reporter.measure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2"); - reporter.measure("measure3", 0.0, 0.0, 5.0, "mark1"); - reporter.measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); - - res = reporter.popPendingEntries(); - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(8, res.entries.size()); - - reporter.getEntries(PerformanceEntryType::MARK); - const auto marks = reporter.getEntries(PerformanceEntryType::MARK); - - const auto measures = reporter.getEntries(PerformanceEntryType::MEASURE); - const auto common_name = reporter.getEntries(std::nullopt, "common_name"); - - reporter.getEntries(); - const auto all = reporter.getEntries(); - - ASSERT_EQ(std::vector({2, 2, 2, 2, 2}), getTypes(measures)); - ASSERT_EQ(std::vector({1, 2}), getTypes(common_name)); - ASSERT_EQ(std::vector({1, 1, 1, 2, 2, 2, 2, 2}), getTypes(all)); - ASSERT_EQ(std::vector({1, 1, 1}), getTypes(marks)); - - ASSERT_EQ( - std::vector({"common_name", "mark1", "mark2"}), - getNames(marks)); - - ASSERT_EQ( - std::vector({"common_name", "common_name"}), - getNames(common_name)); -} - -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearEntries) { - auto& reporter = PerformanceEntryReporter::getInstance(); - - reporter.stopReporting(); - reporter.clearEntries(); - - reporter.startReporting(PerformanceEntryType::MARK); - reporter.startReporting(PerformanceEntryType::MEASURE); - - reporter.mark("common_name", 0.0); - reporter.mark("mark1", 1.0); - reporter.mark("mark2", 2.0); - - reporter.measure("common_name", 0.0, 2.0); - reporter.measure("measure1", 0.0, 2.0, 4.0); - reporter.measure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2"); - reporter.measure("measure3", 0.0, 0.0, 5.0, "mark1"); - reporter.measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); - - reporter.clearEntries(std::nullopt, "common_name"); - auto e1 = reporter.getEntries(); - - ASSERT_EQ(6, e1.size()); - ASSERT_EQ( - std::vector( - {"mark1", "mark2", "measure1", "measure2", "measure3", "measure4"}), - getNames(e1)); - - reporter.clearEntries(PerformanceEntryType::MARK, "mark1"); - auto e2 = reporter.getEntries(); - - ASSERT_EQ(5, e2.size()); - ASSERT_EQ( - std::vector( - {"mark2", "measure1", "measure2", "measure3", "measure4"}), - getNames(e2)); - - reporter.clearEntries(PerformanceEntryType::MEASURE); - auto e3 = reporter.getEntries(); - - ASSERT_EQ(1, e3.size()); - ASSERT_EQ(std::vector({"mark2"}), getNames(e3)); - - reporter.clearEntries(); - auto e4 = reporter.getEntries(); - - ASSERT_EQ(0, e4.size()); -} diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/BoundedConsumableBuffer.h b/packages/react-native/ReactCommon/react/performance/timeline/BoundedConsumableBuffer.h similarity index 100% rename from packages/react-native/ReactCommon/react/nativemodule/webperformance/BoundedConsumableBuffer.h rename to packages/react-native/ReactCommon/react/performance/timeline/BoundedConsumableBuffer.h diff --git a/packages/react-native/ReactCommon/react/performance/timeline/CMakeLists.txt b/packages/react-native/ReactCommon/react/performance/timeline/CMakeLists.txt new file mode 100644 index 00000000000000..720ae9dcdee65e --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"ReactNative\") + +file(GLOB react_performance_timeline_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_performance_timeline SHARED ${react_performance_timeline_SRC}) + +target_include_directories(react_performance_timeline PUBLIC ${REACT_COMMON_DIR}) +target_link_libraries(react_performance_timeline + react_cxxreact) diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp new file mode 100644 index 00000000000000..4adeda741da136 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp @@ -0,0 +1,296 @@ +/* + * 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. + */ + +#include "PerformanceEntryReporter.h" + +#include + +namespace facebook::react { + +std::shared_ptr +PerformanceEntryReporter::getInstance() { + static auto instance = std::make_shared(); + return instance; +} + +PerformanceEntryReporter::PerformanceEntryReporter() { + // For mark entry types we also want to keep the lookup by name, to make + // sure that marks can be referenced by measures + getBuffer(PerformanceEntryType::MARK).hasNameLookup = true; +} + +void PerformanceEntryReporter::setReportingCallback( + std::function callback) { + callback_ = std::move(callback); +} + +DOMHighResTimeStamp PerformanceEntryReporter::getCurrentTimeStamp() const { + return timeStampProvider_ != nullptr ? timeStampProvider_() + : JSExecutor::performanceNow(); +} + +void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) { + auto& buffer = getBuffer(entryType); + buffer.isReporting = true; + buffer.durationThreshold = DEFAULT_DURATION_THRESHOLD; +} + +void PerformanceEntryReporter::setAlwaysLogged( + PerformanceEntryType entryType, + bool isAlwaysLogged) { + auto& buffer = getBuffer(entryType); + buffer.isAlwaysLogged = isAlwaysLogged; +} + +void PerformanceEntryReporter::setDurationThreshold( + PerformanceEntryType entryType, + DOMHighResTimeStamp durationThreshold) { + getBuffer(entryType).durationThreshold = durationThreshold; +} + +void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) { + getBuffer(entryType).isReporting = false; +} + +void PerformanceEntryReporter::stopReporting() { + for (auto& buffer : buffers_) { + buffer.isReporting = false; + } +} + +PerformanceEntryReporter::PopPendingEntriesResult +PerformanceEntryReporter::popPendingEntries() { + std::lock_guard lock(entriesMutex_); + PopPendingEntriesResult res = { + .entries = std::vector(), + .droppedEntriesCount = droppedEntriesCount_}; + for (auto& buffer : buffers_) { + buffer.entries.consume(res.entries); + } + + // Sort by starting time (or ending time, if starting times are equal) + std::stable_sort( + res.entries.begin(), + res.entries.end(), + [](const PerformanceEntry& lhs, const PerformanceEntry& rhs) { + if (lhs.startTime != rhs.startTime) { + return lhs.startTime < rhs.startTime; + } else { + return lhs.duration < rhs.duration; + } + }); + + droppedEntriesCount_ = 0; + return res; +} + +void PerformanceEntryReporter::logEntry(const PerformanceEntry& entry) { + if (entry.entryType == PerformanceEntryType::EVENT) { + eventCounts_[entry.name]++; + } + + if (!isReporting(entry.entryType) && !isAlwaysLogged(entry.entryType)) { + return; + } + + std::lock_guard lock(entriesMutex_); + + auto& buffer = getBuffer(entry.entryType); + + if (entry.duration < buffer.durationThreshold) { + // The entries duration is lower than the desired reporting threshold, skip + return; + } + + if (buffer.hasNameLookup) { + // If we need to remove an entry because the buffer is null, + // we also need to remove it from the name lookup. + auto overwriteCandidate = buffer.entries.getNextOverwriteCandidate(); + if (overwriteCandidate != nullptr) { + std::lock_guard lock2(nameLookupMutex_); + auto it = buffer.nameLookup.find(overwriteCandidate); + if (it != buffer.nameLookup.end() && *it == overwriteCandidate) { + buffer.nameLookup.erase(it); + } + } + } + + auto pushResult = buffer.entries.add(std::move(entry)); + if (pushResult == + BoundedConsumableBuffer::PushStatus::DROP) { + // Start dropping entries once reached maximum buffer size. + // The number of dropped entries will be reported back to the corresponding + // PerformanceObserver callback. + droppedEntriesCount_ += 1; + } + + if (buffer.hasNameLookup) { + std::lock_guard lock2(nameLookupMutex_); + auto currentEntry = &buffer.entries.back(); + auto it = buffer.nameLookup.find(currentEntry); + if (it != buffer.nameLookup.end()) { + buffer.nameLookup.erase(it); + } + buffer.nameLookup.insert(currentEntry); + } + + if (buffer.entries.getNumToConsume() == 1) { + // If the buffer was empty, it signals that JS side just has possibly + // consumed it and is ready to get more + scheduleFlushBuffer(); + } +} + +void PerformanceEntryReporter::mark( + const std::string& name, + const std::optional& startTime) { + logEntry(PerformanceEntry{ + .name = name, + .entryType = PerformanceEntryType::MARK, + .startTime = startTime ? *startTime : getCurrentTimeStamp()}); +} + +void PerformanceEntryReporter::clearEntries( + std::optional entryType, + std::string_view entryName) { + if (!entryType) { + // Clear all entry types + for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { + clearEntries(static_cast(i), entryName); + } + } else { + auto& buffer = getBuffer(*entryType); + if (!entryName.empty()) { + if (buffer.hasNameLookup) { + std::lock_guard lock2(nameLookupMutex_); + buffer.nameLookup.clear(); + } + + std::lock_guard lock(entriesMutex_); + buffer.entries.clear([entryName](const PerformanceEntry& entry) { + return entry.name == entryName; + }); + + if (buffer.hasNameLookup) { + std::lock_guard lock2(nameLookupMutex_); + // BoundedConsumableBuffer::clear() invalidates existing references; we + // need to rebuild the lookup table. If there are multiple entries with + // the same name, make sure the last one gets inserted. + for (int i = static_cast(buffer.entries.size()) - 1; i >= 0; i--) { + const auto& entry = buffer.entries[i]; + buffer.nameLookup.insert(&entry); + } + } + } else { + { + std::lock_guard lock(entriesMutex_); + buffer.entries.clear(); + } + { + std::lock_guard lock2(nameLookupMutex_); + buffer.nameLookup.clear(); + } + } + } +} + +void PerformanceEntryReporter::getEntries( + PerformanceEntryType entryType, + std::string_view entryName, + std::vector& res) const { + std::lock_guard lock(entriesMutex_); + const auto& entries = getBuffer(entryType).entries; + if (entryName.empty()) { + entries.getEntries(res); + } else { + entries.getEntries(res, [entryName](const PerformanceEntry& entry) { + return entry.name == entryName; + }); + } +} + +std::vector PerformanceEntryReporter::getEntries( + std::optional entryType, + std::string_view entryName) const { + std::vector res; + if (!entryType) { + // Collect all entry types + for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { + getEntries(static_cast(i), entryName, res); + } + } else { + getEntries(*entryType, entryName, res); + } + return res; +} + +void PerformanceEntryReporter::measure( + const std::string& name, + DOMHighResTimeStamp startTime, + DOMHighResTimeStamp endTime, + const std::optional& duration, + const std::optional& startMark, + const std::optional& endMark) { + DOMHighResTimeStamp startTimeVal = + startMark ? getMarkTime(*startMark) : startTime; + DOMHighResTimeStamp endTimeVal = endMark ? getMarkTime(*endMark) : endTime; + + if (!endMark && endTime < startTimeVal) { + // The end time is not specified, take the current time, according to the + // standard + endTimeVal = getCurrentTimeStamp(); + } + + DOMHighResTimeStamp durationVal = + duration ? *duration : endTimeVal - startTimeVal; + + logEntry( + {.name = name, + .entryType = PerformanceEntryType::MEASURE, + .startTime = startTimeVal, + .duration = durationVal}); +} + +DOMHighResTimeStamp PerformanceEntryReporter::getMarkTime( + const std::string& markName) const { + PerformanceEntry mark{ + .name = markName, .entryType = PerformanceEntryType::MARK}; + + std::lock_guard lock(nameLookupMutex_); + const auto& marksBuffer = getBuffer(PerformanceEntryType::MARK); + auto it = marksBuffer.nameLookup.find(&mark); + if (it != marksBuffer.nameLookup.end()) { + return (*it)->startTime; + } else { + return 0.0; + } +} + +void PerformanceEntryReporter::logEventEntry( + std::string name, + DOMHighResTimeStamp startTime, + DOMHighResTimeStamp duration, + DOMHighResTimeStamp processingStart, + DOMHighResTimeStamp processingEnd, + uint32_t interactionId) { + logEntry( + {.name = std::move(name), + .entryType = PerformanceEntryType::EVENT, + .startTime = startTime, + .duration = duration, + .processingStart = processingStart, + .processingEnd = processingEnd, + .interactionId = interactionId}); +} + +void PerformanceEntryReporter::scheduleFlushBuffer() { + if (callback_) { + callback_(); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/PerformanceEntryReporter.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h similarity index 65% rename from packages/react-native/ReactCommon/react/nativemodule/webperformance/PerformanceEntryReporter.h rename to packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h index 50e16019cc4498..3a65f02537439f 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/PerformanceEntryReporter.h +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h @@ -7,38 +7,58 @@ #pragma once -#include -#include +#include "BoundedConsumableBuffer.h" + #include #include +#include #include #include #include #include #include -#include "BoundedConsumableBuffer.h" -#include "NativePerformanceObserver.h" - -#include namespace facebook::react { +using DOMHighResTimeStamp = double; + +using PerformanceEntryInteractionId = uint32_t; + +enum class PerformanceEntryType { + // We need to preserve these values for backwards compatibility. + MARK = 1, + MEASURE = 2, + EVENT = 3, + _NEXT = 4, +}; + +struct PerformanceEntry { + std::string name; + PerformanceEntryType entryType; + DOMHighResTimeStamp startTime; + DOMHighResTimeStamp duration = 0; + + // For "event" entries only: + std::optional processingStart; + std::optional processingEnd; + std::optional interactionId; +}; + struct PerformanceEntryHash { - size_t operator()(const RawPerformanceEntry* entry) const { + size_t operator()(const PerformanceEntry* entry) const { return std::hash()(entry->name); } }; struct PerformanceEntryEqual { - bool operator()( - const RawPerformanceEntry* lhs, - const RawPerformanceEntry* rhs) const { + bool operator()(const PerformanceEntry* lhs, const PerformanceEntry* rhs) + const { return lhs->name == rhs->name; } }; using PerformanceEntryRegistryType = std::unordered_set< - const RawPerformanceEntry*, + const PerformanceEntry*, PerformanceEntryHash, PerformanceEntryEqual>; @@ -50,7 +70,7 @@ constexpr double DEFAULT_DURATION_THRESHOLD = 0.0; constexpr size_t DEFAULT_MAX_BUFFER_SIZE = 1024; struct PerformanceEntryBuffer { - BoundedConsumableBuffer entries{DEFAULT_MAX_BUFFER_SIZE}; + BoundedConsumableBuffer entries{DEFAULT_MAX_BUFFER_SIZE}; bool isReporting{false}; bool isAlwaysLogged{false}; double durationThreshold{DEFAULT_DURATION_THRESHOLD}; @@ -58,29 +78,25 @@ struct PerformanceEntryBuffer { PerformanceEntryRegistryType nameLookup; }; -enum class PerformanceEntryType { - // We need to preserve these values for backwards compatibility. - MARK = 1, - MEASURE = 2, - EVENT = 3, - _NEXT = 4, -}; - constexpr size_t NUM_PERFORMANCE_ENTRY_TYPES = (size_t)PerformanceEntryType::_NEXT - 1; // Valid types start from 1. -class PerformanceEntryReporter : public EventLogger, public UIManagerMountHook { +class PerformanceEntryReporter { public: - PerformanceEntryReporter(const PerformanceEntryReporter&) = delete; - void operator=(const PerformanceEntryReporter&) = delete; + PerformanceEntryReporter(); // NOTE: This class is not thread safe, make sure that the calls are made from // the same thread. // TODO: Consider passing it as a parameter to the corresponding modules at // creation time instead of having the singleton. - static PerformanceEntryReporter& getInstance(); + static std::shared_ptr getInstance(); + + struct PopPendingEntriesResult { + std::vector entries; + uint32_t droppedEntriesCount; + }; - void setReportingCallback(std::optional> callback); + void setReportingCallback(std::function callback); void startReporting(PerformanceEntryType entryType); void stopReporting(PerformanceEntryType entryType); void stopReporting(); @@ -89,9 +105,9 @@ class PerformanceEntryReporter : public EventLogger, public UIManagerMountHook { PerformanceEntryType entryType, double durationThreshold); - GetPendingEntriesResult popPendingEntries(); + PopPendingEntriesResult popPendingEntries(); - void logEntry(const RawPerformanceEntry& entry); + void logEntry(const PerformanceEntry& entry); PerformanceEntryBuffer& getBuffer(PerformanceEntryType entryType) { return buffers_[static_cast(entryType) - 1]; @@ -110,8 +126,8 @@ class PerformanceEntryReporter : public EventLogger, public UIManagerMountHook { return getBuffer(entryType).isAlwaysLogged; } - uint32_t getDroppedEntryCount() const { - return droppedEntryCount_; + uint32_t getDroppedEntriesCount() const { + return droppedEntriesCount_; } void mark( @@ -130,7 +146,7 @@ class PerformanceEntryReporter : public EventLogger, public UIManagerMountHook { std::optional entryType = std::nullopt, std::string_view entryName = {}); - std::vector getEntries( + std::vector getEntries( std::optional entryType = std::nullopt, std::string_view entryName = {}) const; @@ -142,66 +158,36 @@ class PerformanceEntryReporter : public EventLogger, public UIManagerMountHook { double processingEnd, uint32_t interactionId); - EventTag onEventStart(std::string_view name) override; - void onEventProcessingStart(EventTag tag) override; - void onEventProcessingEnd(EventTag tag) override; - - void shadowTreeDidMount( - const RootShadowNode::Shared& rootShadowNode, - double mountTime) noexcept override; - const std::unordered_map& getEventCounts() const { return eventCounts_; } + DOMHighResTimeStamp getCurrentTimeStamp() const; + void setTimeStampProvider(std::function provider) { - timeStampProvider_ = provider; + timeStampProvider_ = std::move(provider); } private: - std::optional> callback_; + std::function callback_; mutable std::mutex entriesMutex_; std::array buffers_; std::unordered_map eventCounts_; - uint32_t droppedEntryCount_{0}; - - struct EventEntry { - std::string_view name; - double startTime{0.0}; - double processingStartTime{0.0}; - double processingEndTime{0.0}; - - // TODO: Define the way to assign interaction IDs to the event chains - // (T141358175) - uint32_t interactionId{0}; - }; - - // Registry to store the events that are currently ongoing. - // Note that we could probably use a more efficient container for that, - // but since we only report discrete events, the volume is normally low, - // so a hash map should be just fine. - std::unordered_map eventsInFlight_; - mutable std::mutex eventsInFlightMutex_; + uint32_t droppedEntriesCount_{0}; std::function timeStampProvider_ = nullptr; mutable std::mutex nameLookupMutex_; - static EventTag sCurrentEventTag_; - - PerformanceEntryReporter(); - double getMarkTime(const std::string& markName) const; void scheduleFlushBuffer(); void getEntries( PerformanceEntryType entryType, std::string_view entryName, - std::vector& res) const; - - double getCurrentTimeStamp() const; + std::vector& res) const; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/React-performancetimeline.podspec b/packages/react-native/ReactCommon/react/performance/timeline/React-performancetimeline.podspec new file mode 100644 index 00000000000000..1802a6ea7b2785 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/React-performancetimeline.podspec @@ -0,0 +1,56 @@ +# 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. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +folly_config = get_folly_config() +folly_compiler_flags = folly_config[:compiler_flags] +folly_version = folly_config[:version] + +header_search_paths = [ + "\"$(PODS_ROOT)/RCT-Folly\"", + "\"$(PODS_ROOT)/boost\"", +] + +if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../../..\"" # this is needed to allow the performancetimeline access its own files +end + +Pod::Spec.new do |s| + s.name = "React-performancetimeline" + s.version = version + s.summary = "React performance timeline utilities" + s.homepage = "https://reactnative.dev/" + s.license = package["license"] + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = min_supported_versions + s.source = source + s.source_files = "**/*.{cpp,h}" + s.compiler_flags = folly_compiler_flags + s.header_dir = "react/performance/timeline" + s.exclude_files = "tests" + s.pod_target_xcconfig = { + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "HEADER_SEARCH_PATHS" => header_search_paths.join(' ')} + + if ENV['USE_FRAMEWORKS'] + s.module_name = "React_performancetimeline" + s.header_mappings_dir = "../../.." + end + + s.dependency "React-cxxreact" + s.dependency "RCT-Folly", folly_version +end diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/tests/BoundedConsumableBufferTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/BoundedConsumableBufferTest.cpp similarity index 100% rename from packages/react-native/ReactCommon/react/nativemodule/webperformance/tests/BoundedConsumableBufferTest.cpp rename to packages/react-native/ReactCommon/react/performance/timeline/tests/BoundedConsumableBufferTest.cpp diff --git a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp new file mode 100644 index 00000000000000..65cb1a97887b25 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp @@ -0,0 +1,368 @@ +/* + * 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. + */ + +#include + +#include + +#include "../PerformanceEntryReporter.h" + +namespace facebook::react { + +[[maybe_unused]] static bool operator==( + const PerformanceEntry& lhs, + const PerformanceEntry& rhs) { + return lhs.name == rhs.name && lhs.entryType == rhs.entryType && + lhs.startTime == rhs.startTime && lhs.duration == rhs.duration && + lhs.processingStart == rhs.processingStart && + lhs.processingEnd == rhs.processingEnd && + lhs.interactionId == rhs.interactionId; +} + +[[maybe_unused]] static std::ostream& operator<<( + std::ostream& os, + const PerformanceEntry& entry) { + static constexpr const char* entryTypeNames[] = { + "UNDEFINED", + "MARK", + "MEASURE", + "EVENT", + }; + return os << "{ name: " << entry.name + << ", type: " << entryTypeNames[static_cast(entry.entryType)] + << ", startTime: " << entry.startTime + << ", duration: " << entry.duration << " }"; +} +} // namespace facebook::react + +using namespace facebook::react; + +TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStartReporting) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->stopReporting(); + reporter->clearEntries(); + + reporter->startReporting(PerformanceEntryType::MARK); + reporter->startReporting(PerformanceEntryType::MEASURE); + + ASSERT_TRUE(reporter->isReporting(PerformanceEntryType::MARK)); + ASSERT_TRUE(reporter->isReporting(PerformanceEntryType::MEASURE)); + + ASSERT_FALSE(reporter->isReporting(PerformanceEntryType::EVENT)); +} + +TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStopReporting) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->stopReporting(); + reporter->clearEntries(); + + reporter->startReporting(PerformanceEntryType::MARK); + + reporter->mark("mark0", 0.0); + reporter->mark("mark1", 0.0); + reporter->mark("mark2", 0.0); + reporter->measure("measure0", 0.0, 0.0); + + auto res = reporter->popPendingEntries(); + const auto& entries = res.entries; + + ASSERT_EQ(0, res.droppedEntriesCount); + ASSERT_EQ(3, entries.size()); + + res = reporter->popPendingEntries(); + + ASSERT_EQ(0, res.droppedEntriesCount); + ASSERT_EQ(0, res.entries.size()); + + reporter->stopReporting(PerformanceEntryType::MARK); + reporter->startReporting(PerformanceEntryType::MEASURE); + + reporter->mark("mark3"); + reporter->measure("measure1", 0.0, 0.0); + + res = reporter->popPendingEntries(); + + ASSERT_EQ(0, res.droppedEntriesCount); + ASSERT_EQ(1, res.entries.size()); + ASSERT_STREQ("measure1", res.entries[0].name.c_str()); +} + +TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMarks) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->stopReporting(); + reporter->clearEntries(); + + reporter->startReporting(PerformanceEntryType::MARK); + + reporter->mark("mark0", 0.0); + reporter->mark("mark1", 1.0); + reporter->mark("mark2", 2.0); + // Report mark0 again + reporter->mark("mark0", 3.0); + + auto res = reporter->popPendingEntries(); + const auto& entries = res.entries; + + ASSERT_EQ(0, res.droppedEntriesCount); + ASSERT_EQ(4, entries.size()); + + const std::vector expected = { + {.name = "mark0", + .entryType = PerformanceEntryType::MARK, + .startTime = 0.0}, + {.name = "mark1", + .entryType = PerformanceEntryType::MARK, + .startTime = 1.0}, + {.name = "mark2", + .entryType = PerformanceEntryType::MARK, + .startTime = 2.0}, + {.name = "mark0", + .entryType = PerformanceEntryType::MARK, + .startTime = 3.0}, + }; + + ASSERT_EQ(expected, entries); +} + +TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->stopReporting(); + reporter->clearEntries(); + + reporter->startReporting(PerformanceEntryType::MARK); + reporter->startReporting(PerformanceEntryType::MEASURE); + + reporter->mark("mark0", 0.0); + reporter->mark("mark1", 1.0); + reporter->mark("mark2", 2.0); + + reporter->measure("measure0", 0.0, 2.0); + reporter->measure("measure1", 0.0, 2.0, 4.0); + reporter->measure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2"); + reporter->measure("measure3", 0.0, 0.0, 5.0, "mark1"); + reporter->measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); + + reporter->setTimeStampProvider([]() { return 3.5; }); + reporter->measure("measure5", 0.0, 0.0, std::nullopt, "mark2"); + + reporter->mark("mark3", 2.0); + reporter->measure("measure6", 2.0, 2.0); + reporter->mark("mark4", 2.0); + reporter->mark("mark4", 3.0); + // Uses the last reported time for mark4 + reporter->measure("measure7", 0.0, 0.0, std::nullopt, "mark1", "mark4"); + + auto res = reporter->popPendingEntries(); + const auto& entries = res.entries; + + ASSERT_EQ(0, res.droppedEntriesCount); + + const std::vector expected = { + {.name = "mark0", + .entryType = PerformanceEntryType::MARK, + .startTime = 0.0}, + {.name = "measure0", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0.0, + .duration = 2.0}, + {.name = "measure1", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0.0, + .duration = 4.0}, + {.name = "mark1", + .entryType = PerformanceEntryType::MARK, + .startTime = 1.0}, + {.name = "measure2", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.0, + .duration = 1.0}, + {.name = "measure7", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.0, + .duration = 2.0}, + {.name = "measure3", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.0, + .duration = 5.0}, + {.name = "measure4", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.5, + .duration = 0.5}, + {.name = "mark2", + .entryType = PerformanceEntryType::MARK, + .startTime = 2.0}, + {.name = "mark3", + .entryType = PerformanceEntryType::MARK, + .startTime = 2.0}, + {.name = "mark4", + .entryType = PerformanceEntryType::MARK, + .startTime = 2.0}, + {.name = "measure6", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 2.0, + .duration = 0.0}, + {.name = "measure5", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 2.0, + .duration = 1.5}, + {.name = "mark4", + .entryType = PerformanceEntryType::MARK, + .startTime = 3.0}}; + + ASSERT_EQ(expected, entries); +} + +static std::vector getNames( + const std::vector& entries) { + std::vector res; + std::transform( + entries.begin(), + entries.end(), + std::back_inserter(res), + [](const PerformanceEntry& e) { return e.name; }); + return res; +} + +static std::vector getTypes( + const std::vector& entries) { + std::vector res; + std::transform( + entries.begin(), + entries.end(), + std::back_inserter(res), + [](const PerformanceEntry& e) { return e.entryType; }); + return res; +} + +TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->stopReporting(); + reporter->clearEntries(); + + auto res = reporter->popPendingEntries(); + const auto& entries = res.entries; + + ASSERT_EQ(0, res.droppedEntriesCount); + ASSERT_EQ(0, entries.size()); + + reporter->startReporting(PerformanceEntryType::MARK); + reporter->startReporting(PerformanceEntryType::MEASURE); + + reporter->mark("common_name", 0.0); + reporter->mark("mark1", 1.0); + reporter->mark("mark2", 2.0); + + reporter->measure("common_name", 0.0, 2.0); + reporter->measure("measure1", 0.0, 2.0, 4.0); + reporter->measure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2"); + reporter->measure("measure3", 0.0, 0.0, 5.0, "mark1"); + reporter->measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); + + res = reporter->popPendingEntries(); + ASSERT_EQ(0, res.droppedEntriesCount); + ASSERT_EQ(8, res.entries.size()); + + reporter->getEntries(PerformanceEntryType::MARK); + const auto marks = reporter->getEntries(PerformanceEntryType::MARK); + + const auto measures = reporter->getEntries(PerformanceEntryType::MEASURE); + const auto common_name = reporter->getEntries(std::nullopt, "common_name"); + + reporter->getEntries(); + const auto all = reporter->getEntries(); + + ASSERT_EQ( + std::vector( + {PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE}), + getTypes(measures)); + ASSERT_EQ( + std::vector({PerformanceEntryType::MARK, PerformanceEntryType::MEASURE}), + getTypes(common_name)); + ASSERT_EQ( + std::vector( + {PerformanceEntryType::MARK, + PerformanceEntryType::MARK, + PerformanceEntryType::MARK, + PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE, + PerformanceEntryType::MEASURE}), + getTypes(all)); + ASSERT_EQ( + std::vector( + {PerformanceEntryType::MARK, + PerformanceEntryType::MARK, + PerformanceEntryType::MARK}), + getTypes(marks)); + + ASSERT_EQ( + std::vector({"common_name", "mark1", "mark2"}), + getNames(marks)); + + ASSERT_EQ( + std::vector({"common_name", "common_name"}), + getNames(common_name)); +} + +TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearEntries) { + auto reporter = PerformanceEntryReporter::getInstance(); + + reporter->stopReporting(); + reporter->clearEntries(); + + reporter->startReporting(PerformanceEntryType::MARK); + reporter->startReporting(PerformanceEntryType::MEASURE); + + reporter->mark("common_name", 0.0); + reporter->mark("mark1", 1.0); + reporter->mark("mark2", 2.0); + + reporter->measure("common_name", 0.0, 2.0); + reporter->measure("measure1", 0.0, 2.0, 4.0); + reporter->measure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2"); + reporter->measure("measure3", 0.0, 0.0, 5.0, "mark1"); + reporter->measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); + + reporter->clearEntries(std::nullopt, "common_name"); + auto e1 = reporter->getEntries(); + + ASSERT_EQ(6, e1.size()); + ASSERT_EQ( + std::vector( + {"mark1", "mark2", "measure1", "measure2", "measure3", "measure4"}), + getNames(e1)); + + reporter->clearEntries(PerformanceEntryType::MARK, "mark1"); + auto e2 = reporter->getEntries(); + + ASSERT_EQ(5, e2.size()); + ASSERT_EQ( + std::vector( + {"mark2", "measure1", "measure2", "measure3", "measure4"}), + getNames(e2)); + + reporter->clearEntries(PerformanceEntryType::MEASURE); + auto e3 = reporter->getEntries(); + + ASSERT_EQ(1, e3.size()); + ASSERT_EQ(std::vector({"mark2"}), getNames(e3)); + + reporter->clearEntries(); + auto e4 = reporter->getEntries(); + + ASSERT_EQ(0, e4.size()); +} diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp index 2c8175dc0aa834..5b95b5d62b30f8 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp @@ -9,7 +9,6 @@ #include #include #include -#include "EventLogger.h" #include "EventQueue.h" #include "RawEvent.h" @@ -21,12 +20,14 @@ EventDispatcher::EventDispatcher( const EventBeat::Factory& asynchronousEventBeatFactory, const EventBeat::SharedOwnerBox& ownerBox, RuntimeScheduler& runtimeScheduler, - StatePipe statePipe) + StatePipe statePipe, + std::weak_ptr eventLogger) : eventQueue_(EventQueue( eventProcessor, asynchronousEventBeatFactory(ownerBox), runtimeScheduler)), - statePipe_(std::move(statePipe)) {} + statePipe_(std::move(statePipe)), + eventLogger_(std::move(eventLogger)) {} void EventDispatcher::dispatchEvent(RawEvent&& rawEvent) const { // Allows the event listener to interrupt default event dispatch @@ -34,7 +35,7 @@ void EventDispatcher::dispatchEvent(RawEvent&& rawEvent) const { return; } - auto eventLogger = getEventLogger(); + auto eventLogger = eventLogger_.lock(); if (eventLogger != nullptr) { rawEvent.loggingTag = eventLogger->onEventStart(rawEvent.type); } diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h index d0331166cef4b8..f339ba6d8313d5 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h @@ -9,10 +9,12 @@ #include #include +#include #include #include #include #include +#include namespace facebook::react { @@ -33,7 +35,8 @@ class EventDispatcher { const EventBeat::Factory& asynchronousEventBeatFactory, const EventBeat::SharedOwnerBox& ownerBox, RuntimeScheduler& runtimeScheduler, - StatePipe statePipe); + StatePipe statePipe, + std::weak_ptr eventLogger); /* * Dispatches a raw event with given priority using event-delivery pipe. @@ -74,6 +77,7 @@ class EventDispatcher { const StatePipe statePipe_; mutable EventListenerContainer eventListeners_; + const std::weak_ptr eventLogger_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventLogger.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventLogger.cpp deleted file mode 100644 index 2bb552ea9b8216..00000000000000 --- a/packages/react-native/ReactCommon/react/renderer/core/EventLogger.cpp +++ /dev/null @@ -1,22 +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. - */ - -#include "EventLogger.h" - -namespace facebook::react { - -EventLogger* eventLogger; - -void setEventLogger(EventLogger* logger) { - eventLogger = logger; -} - -EventLogger* getEventLogger() { - return eventLogger; -} - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventLogger.h b/packages/react-native/ReactCommon/react/renderer/core/EventLogger.h index ba0cabd17764a3..d0fddea4ba7579 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventLogger.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventLogger.h @@ -12,6 +12,7 @@ namespace facebook::react { using EventTag = unsigned int; +const EventTag EMPTY_EVENT_TAG = 0; /* * Interface for logging discrete events (such as pointerenter/leave), @@ -40,7 +41,4 @@ class EventLogger { virtual void onEventProcessingEnd(EventTag tag) = 0; }; -void setEventLogger(EventLogger* eventLogger); -EventLogger* getEventLogger(); - } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp index a49882f438bcd6..6866422b17f328 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp @@ -17,10 +17,12 @@ namespace facebook::react { EventQueueProcessor::EventQueueProcessor( EventPipe eventPipe, EventPipeConclusion eventPipeConclusion, - StatePipe statePipe) + StatePipe statePipe, + std::weak_ptr eventLogger) : eventPipe_(std::move(eventPipe)), eventPipeConclusion_(std::move(eventPipeConclusion)), - statePipe_(std::move(statePipe)) {} + statePipe_(std::move(statePipe)), + eventLogger_(std::move(eventLogger)) {} void EventQueueProcessor::flushEvents( jsi::Runtime& runtime, @@ -52,7 +54,7 @@ void EventQueueProcessor::flushEvents( reactPriority = ReactEventPriority::Discrete; } - auto eventLogger = getEventLogger(); + auto eventLogger = eventLogger_.lock(); if (eventLogger != nullptr) { eventLogger->onEventProcessingStart(event.loggingTag); } diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.h b/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.h index bfcad84c155862..5c1c9f2b0d1eac 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.h @@ -22,7 +22,8 @@ class EventQueueProcessor { EventQueueProcessor( EventPipe eventPipe, EventPipeConclusion eventPipeConclusion, - StatePipe statePipe); + StatePipe statePipe, + std::weak_ptr eventLogger); void flushEvents(jsi::Runtime& runtime, std::vector&& events) const; void flushStateUpdates(std::vector&& states) const; @@ -31,6 +32,7 @@ class EventQueueProcessor { const EventPipe eventPipe_; const EventPipeConclusion eventPipeConclusion_; const StatePipe statePipe_; + const std::weak_ptr eventLogger_; mutable bool hasContinuousEventStarted_{false}; }; diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp index 1d673222b93570..eaa4404e768696 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp @@ -8,15 +8,25 @@ #include #include #include +#include #include #include #include #include #include +#include namespace facebook::react { +class MockEventLogger : public EventLogger { + EventTag onEventStart(std::string_view /*name*/) override { + return EMPTY_EVENT_TAG; + } + void onEventProcessingStart(EventTag /*tag*/) override {} + void onEventProcessingEnd(EventTag /*tag*/) override {} +}; + class EventQueueProcessorTest : public testing::Test { protected: void SetUp() override { @@ -34,9 +44,10 @@ class EventQueueProcessorTest : public testing::Test { auto dummyEventPipeConclusion = [](jsi::Runtime& runtime) {}; auto dummyStatePipe = [](const StateUpdate& stateUpdate) {}; + auto mockEventLogger = std::make_shared(); eventProcessor_ = std::make_unique( - eventPipe, dummyEventPipeConclusion, dummyStatePipe); + eventPipe, dummyEventPipeConclusion, dummyStatePipe, mockEventLogger); } std::unique_ptr runtime_; diff --git a/packages/react-native/ReactCommon/react/renderer/observers/events/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/observers/events/CMakeLists.txt new file mode 100644 index 00000000000000..5c5e61f3658d81 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/events/CMakeLists.txt @@ -0,0 +1,25 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"Fabric\") + +file(GLOB react_render_observers_events_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_render_observers_events SHARED ${react_render_observers_events_SRC}) + +target_include_directories(react_render_observers_events PUBLIC ${REACT_COMMON_DIR}) +target_link_libraries(react_render_observers_events + react_performance_timeline + react_render_core + react_render_uimanager + react_utils) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/events/EventPerformanceLogger.cpp b/packages/react-native/ReactCommon/react/renderer/observers/events/EventPerformanceLogger.cpp new file mode 100644 index 00000000000000..73cf365de4150c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/events/EventPerformanceLogger.cpp @@ -0,0 +1,200 @@ +/* + * 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. + */ + +#include "EventPerformanceLogger.h" + +#include +#include + +namespace facebook::react { + +namespace { + +struct StrKey { + uint32_t key; + StrKey(std::string_view s) : key(std::hash{}(s)) {} + + bool operator==(const StrKey& rhs) const { + return key == rhs.key; + } +}; + +struct StrKeyHash { + constexpr size_t operator()(const StrKey& strKey) const { + return static_cast(strKey.key); + } +}; + +// Supported events for reporting, see +// https://www.w3.org/TR/event-timing/#sec-events-exposed +// Not all of these are currently supported by RN, but we map them anyway for +// future-proofing. +using SupportedEventTypeRegistry = + std::unordered_map; + +const SupportedEventTypeRegistry& getSupportedEvents() { + static SupportedEventTypeRegistry SUPPORTED_EVENTS = { + {StrKey("topAuxClick"), "auxclick"}, + {StrKey("topClick"), "click"}, + {StrKey("topContextMenu"), "contextmenu"}, + {StrKey("topDblClick"), "dblclick"}, + {StrKey("topMouseDown"), "mousedown"}, + {StrKey("topMouseEnter"), "mouseenter"}, + {StrKey("topMouseLeave"), "mouseleave"}, + {StrKey("topMouseOut"), "mouseout"}, + {StrKey("topMouseOver"), "mouseover"}, + {StrKey("topMouseUp"), "mouseup"}, + {StrKey("topPointerOver"), "pointerover"}, + {StrKey("topPointerEnter"), "pointerenter"}, + {StrKey("topPointerDown"), "pointerdown"}, + {StrKey("topPointerUp"), "pointerup"}, + {StrKey("topPointerCancel"), "pointercancel"}, + {StrKey("topPointerOut"), "pointerout"}, + {StrKey("topPointerLeave"), "pointerleave"}, + {StrKey("topGotPointerCapture"), "gotpointercapture"}, + {StrKey("topLostPointerCapture"), "lostpointercapture"}, + {StrKey("topTouchStart"), "touchstart"}, + {StrKey("topTouchEnd"), "touchend"}, + {StrKey("topTouchCancel"), "touchcancel"}, + {StrKey("topKeyDown"), "keydown"}, + {StrKey("topKeyPress"), "keypress"}, + {StrKey("topKeyUp"), "keyup"}, + {StrKey("topBeforeInput"), "beforeinput"}, + {StrKey("topInput"), "input"}, + {StrKey("topCompositionStart"), "compositionstart"}, + {StrKey("topCompositionUpdate"), "compositionupdate"}, + {StrKey("topCompositionEnd"), "compositionend"}, + {StrKey("topDragStart"), "dragstart"}, + {StrKey("topDragEnd"), "dragend"}, + {StrKey("topDragEnter"), "dragenter"}, + {StrKey("topDragLeave"), "dragleave"}, + {StrKey("topDragOver"), "dragover"}, + {StrKey("topDrop"), "drop"}, + }; + return SUPPORTED_EVENTS; +} + +} // namespace + +EventPerformanceLogger::EventPerformanceLogger( + std::weak_ptr performanceEntryReporter) + : performanceEntryReporter_(std::move(performanceEntryReporter)) {} + +EventTag EventPerformanceLogger::onEventStart(std::string_view name) { + auto performanceEntryReporter = performanceEntryReporter_.lock(); + if (performanceEntryReporter == nullptr) { + return EMPTY_EVENT_TAG; + } + + const auto& supportedEvents = getSupportedEvents(); + auto it = supportedEvents.find(name); + if (it == supportedEvents.end()) { + return 0; + } + + auto reportedName = it->second; + + auto eventTag = createEventTag(); + + auto timeStamp = performanceEntryReporter->getCurrentTimeStamp(); + { + std::lock_guard lock(eventsInFlightMutex_); + eventsInFlight_.emplace(eventTag, EventEntry{reportedName, timeStamp, 0.0}); + } + return eventTag; +} + +void EventPerformanceLogger::onEventProcessingStart(EventTag tag) { + auto performanceEntryReporter = performanceEntryReporter_.lock(); + if (performanceEntryReporter == nullptr) { + return; + } + + auto timeStamp = performanceEntryReporter->getCurrentTimeStamp(); + { + std::lock_guard lock(eventsInFlightMutex_); + auto it = eventsInFlight_.find(tag); + if (it != eventsInFlight_.end()) { + it->second.processingStartTime = timeStamp; + } + } +} + +void EventPerformanceLogger::onEventProcessingEnd(EventTag tag) { + auto performanceEntryReporter = performanceEntryReporter_.lock(); + if (performanceEntryReporter == nullptr) { + return; + } + + auto timeStamp = performanceEntryReporter->getCurrentTimeStamp(); + { + std::lock_guard lock(eventsInFlightMutex_); + auto it = eventsInFlight_.find(tag); + if (it == eventsInFlight_.end()) { + return; + } + auto& entry = it->second; + entry.processingEndTime = timeStamp; + + if (CoreFeatures::enableReportEventPaintTime) { + // If reporting paint time, don't send the entry just yet and wait for the + // mount hook callback to be called + return; + } + + const auto& name = entry.name; + + performanceEntryReporter->logEventEntry( + std::string(name), + entry.startTime, + timeStamp - entry.startTime, + entry.processingStartTime, + entry.processingEndTime, + entry.interactionId); + eventsInFlight_.erase(it); + } +} + +void EventPerformanceLogger::shadowTreeDidMount( + const RootShadowNode::Shared& /*rootShadowNode*/, + double mountTime) noexcept { + if (!CoreFeatures::enableReportEventPaintTime) { + return; + } + + auto performanceEntryReporter = performanceEntryReporter_.lock(); + if (performanceEntryReporter == nullptr) { + return; + } + + std::lock_guard lock(eventsInFlightMutex_); + auto it = eventsInFlight_.begin(); + while (it != eventsInFlight_.end()) { + const auto& entry = it->second; + if (entry.processingEndTime == 0.0 || entry.processingEndTime > mountTime) { + // This mount doesn't correspond to the event + ++it; + continue; + } + + performanceEntryReporter->logEventEntry( + std::string(entry.name), + entry.startTime, + mountTime - entry.startTime, + entry.processingStartTime, + entry.processingEndTime, + entry.interactionId); + it = eventsInFlight_.erase(it); + } +} + +EventTag EventPerformanceLogger::createEventTag() { + sCurrentEventTag_++; + return sCurrentEventTag_; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/events/EventPerformanceLogger.h b/packages/react-native/ReactCommon/react/renderer/observers/events/EventPerformanceLogger.h new file mode 100644 index 00000000000000..9badd9c758a2fe --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/events/EventPerformanceLogger.h @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class EventPerformanceLogger : public EventLogger, public UIManagerMountHook { + public: + explicit EventPerformanceLogger( + std::weak_ptr performanceEntryReporter); + +#pragma mark - EventLogger + + EventTag onEventStart(std::string_view name) override; + void onEventProcessingStart(EventTag tag) override; + void onEventProcessingEnd(EventTag tag) override; + +#pragma mark - UIManagerMountHook + + void shadowTreeDidMount( + const RootShadowNode::Shared& rootShadowNode, + double mountTime) noexcept override; + + private: + struct EventEntry { + std::string_view name; + DOMHighResTimeStamp startTime{0.0}; + DOMHighResTimeStamp processingStartTime{0.0}; + DOMHighResTimeStamp processingEndTime{0.0}; + + // TODO: Define the way to assign interaction IDs to the event chains + // (T141358175) + PerformanceEntryInteractionId interactionId{0}; + }; + + // Registry to store the events that are currently ongoing. + // Note that we could probably use a more efficient container for that, + // but since we only report discrete events, the volume is normally low, + // so a hash map should be just fine. + std::unordered_map eventsInFlight_; + std::mutex eventsInFlightMutex_; + + std::weak_ptr performanceEntryReporter_; + + EventTag sCurrentEventTag_{EMPTY_EVENT_TAG}; + + EventTag createEventTag(); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index 8cc6a6deebb00f..a38be770cb1243 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -71,7 +71,7 @@ bool RuntimeScheduler_Modern::getShouldYield() const noexcept { std::shared_lock lock(schedulingMutex_); return syncTaskRequests_ > 0 || - (!taskQueue_.empty() && taskQueue_.top() != currentTask_); + (!taskQueue_.empty() && taskQueue_.top().get() != currentTask_); } void RuntimeScheduler_Modern::cancelTask(Task& task) noexcept { @@ -105,9 +105,8 @@ void RuntimeScheduler_Modern::executeNowOnTheSameThread( auto priority = SchedulerPriority::ImmediatePriority; auto expirationTime = currentTime + timeoutForSchedulerPriority(priority); - auto task = std::make_shared( - priority, std::move(callback), expirationTime); + auto task = Task{priority, std::move(callback), expirationTime}; executeTask(runtime, task, currentTime); }); @@ -207,7 +206,7 @@ void RuntimeScheduler_Modern::startWorkLoop( break; } - executeTask(runtime, topPriorityTask, currentTime); + executeTask(runtime, *topPriorityTask, currentTime); } } catch (jsi::JSError& error) { handleJSError(runtime, error, true); @@ -245,19 +244,19 @@ std::shared_ptr RuntimeScheduler_Modern::selectTask( void RuntimeScheduler_Modern::executeTask( jsi::Runtime& runtime, - const std::shared_ptr& task, + Task& task, RuntimeSchedulerTimePoint currentTime) { - auto didUserCallbackTimeout = task->expirationTime <= currentTime; + auto didUserCallbackTimeout = task.expirationTime <= currentTime; SystraceSection s( "RuntimeScheduler::executeTask", "priority", - serialize(task->priority), + serialize(task.priority), "didUserCallbackTimeout", didUserCallbackTimeout); - currentTask_ = task; - currentPriority_ = task->priority; + currentTask_ = &task; + currentPriority_ = task.priority; { ScopedShadowTreeRevisionLock revisionLock( @@ -275,6 +274,8 @@ void RuntimeScheduler_Modern::executeTask( updateRendering(); } } + + currentTask_ = nullptr; } /** @@ -296,16 +297,16 @@ void RuntimeScheduler_Modern::updateRendering() { void RuntimeScheduler_Modern::executeMacrotask( jsi::Runtime& runtime, - std::shared_ptr task, + Task& task, bool didUserCallbackTimeout) const { SystraceSection s("RuntimeScheduler::executeMacrotask"); - auto result = task->execute(runtime, didUserCallbackTimeout); + auto result = task.execute(runtime, didUserCallbackTimeout); if (result.isObject() && result.getObject(runtime).isFunction(runtime)) { // If the task returned a continuation callback, we re-assign it to the task // and keep the task in the queue. - task->callback = result.getObject(runtime).getFunction(runtime); + task.callback = result.getObject(runtime).getFunction(runtime); } } diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h index f730509928ba73..fa9b6c8c8e75d3 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -135,7 +135,7 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { TaskPriorityComparer> taskQueue_; - std::shared_ptr currentTask_; + Task* currentTask_{}; /** * This protects the access to `taskQueue_` and `isWorkLoopScheduled_`. @@ -162,12 +162,12 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { */ void executeTask( jsi::Runtime& runtime, - const std::shared_ptr& task, + Task& task, RuntimeSchedulerTimePoint currentTime); void executeMacrotask( jsi::Runtime& runtime, - std::shared_ptr task, + Task& task, bool didUserCallbackTimeout) const; void updateRendering(); diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt index 44fda8bc85fced..67aed63f79440d 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt @@ -26,11 +26,13 @@ target_link_libraries(react_render_scheduler react_config react_debug react_featureflags + react_performance_timeline react_render_componentregistry react_render_core react_render_debug react_render_graphics react_render_mounting + react_render_observers_events react_render_runtimescheduler react_render_uimanager react_utils diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 7a34d992dcd68f..e85ae3d2cc8101 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -43,6 +43,14 @@ Scheduler::Scheduler( // Creating a container for future `EventDispatcher` instance. eventDispatcher_ = std::make_shared>(); + // TODO(T182293888): remove singleton from PerformanceEntryReporter and move + // creation here. + auto performanceEntryReporter = PerformanceEntryReporter::getInstance(); + performanceEntryReporter_ = performanceEntryReporter; + + eventPerformanceLogger_ = + std::make_shared(performanceEntryReporter_); + auto uiManager = std::make_shared( runtimeExecutor_, schedulerToolbox.backgroundExecutor, contextContainer_); auto eventOwnerBox = std::make_shared(); @@ -88,11 +96,13 @@ Scheduler::Scheduler( // Creating an `EventDispatcher` instance inside the already allocated // container (inside the optional). eventDispatcher_->emplace( - EventQueueProcessor(eventPipe, eventPipeConclusion, statePipe), + EventQueueProcessor( + eventPipe, eventPipeConclusion, statePipe, eventPerformanceLogger_), schedulerToolbox.asynchronousEventBeatFactory, eventOwnerBox, *runtimeScheduler, - statePipe); + statePipe, + eventPerformanceLogger_); // Casting to `std::shared_ptr`. auto eventDispatcher = @@ -150,6 +160,10 @@ Scheduler::Scheduler( CoreFeatures::enableReportEventPaintTime = reactNativeConfig_->getBool( "rn_responsiveness_performance:enable_paint_time_reporting"); + + if (CoreFeatures::enableReportEventPaintTime) { + uiManager->registerMountHook(*eventPerformanceLogger_); + } } Scheduler::~Scheduler() { diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index 2df6472ab6a68f..265aebc9a65461 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -131,6 +133,9 @@ class Scheduler final : public UIManagerDelegate { */ std::shared_ptr> eventDispatcher_; + std::shared_ptr performanceEntryReporter_; + std::shared_ptr eventPerformanceLogger_; + /** * Hold onto ContextContainer. See SchedulerToolbox. * Must not be nullptr. diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 84623bd12374b3..72ac1af94e4738 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -145,6 +145,7 @@ def use_react_native! ( pod 'React-jsinspector', :path => "#{prefix}/ReactCommon/jsinspector-modern" pod 'React-callinvoker', :path => "#{prefix}/ReactCommon/callinvoker" + pod 'React-performancetimeline', :path => "#{prefix}/ReactCommon/react/performance/timeline" pod 'React-runtimeexecutor', :path => "#{prefix}/ReactCommon/runtimeexecutor" pod 'React-runtimescheduler', :path => "#{prefix}/ReactCommon/react/renderer/runtimescheduler" pod 'React-rendererdebug', :path => "#{prefix}/ReactCommon/react/renderer/debug" diff --git a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js index 980fd2d55aaa27..a31edf4a3534db 100644 --- a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js +++ b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js @@ -100,11 +100,17 @@ const onPerformanceEntry = () => { const durationThreshold = observerConfig.entryTypes.get(entry.entryType); return entry.duration >= (durationThreshold ?? 0); }); - observerConfig.callback( - new PerformanceObserverEntryList(entriesForObserver), - observer, - droppedEntriesCount, - ); + if (entriesForObserver.length !== 0) { + try { + observerConfig.callback( + new PerformanceObserverEntryList(entriesForObserver), + observer, + droppedEntriesCount, + ); + } catch (error) { + console.error(error); + } + } } }; diff --git a/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js b/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js index cf46dcbfd0e2d8..06cfbff59cd8d5 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js @@ -205,4 +205,58 @@ describe('PerformanceObserver', () => { 'mark7', ]); }); + + it('should guard against errors in observer callbacks', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const observer1Callback = jest.fn(() => { + throw new Error('observer 1 callback'); + }); + const observer1 = new PerformanceObserver(observer1Callback); + + const observer2Callback = jest.fn(); + const observer2 = new PerformanceObserver(observer2Callback); + + observer1.observe({type: 'mark'}); + observer2.observe({type: 'mark'}); + + NativePerformanceObserver.logRawEntry({ + name: 'mark1', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 200, + }); + + jest.runAllTicks(); + + expect(observer1Callback).toHaveBeenCalled(); + expect(observer2Callback).toHaveBeenCalled(); + + expect(console.error).toHaveBeenCalledWith( + new Error('observer 1 callback'), + ); + }); + + it('should not invoke observers with non-matching entries', () => { + const observer1Callback = jest.fn(); + const observer1 = new PerformanceObserver(observer1Callback); + + const observer2Callback = jest.fn(); + const observer2 = new PerformanceObserver(observer2Callback); + + observer1.observe({type: 'mark'}); + observer2.observe({type: 'measure'}); + + NativePerformanceObserver.logRawEntry({ + name: 'mark1', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 200, + }); + + jest.runAllTicks(); + + expect(observer1Callback).toHaveBeenCalled(); + expect(observer2Callback).not.toHaveBeenCalled(); + }); }); diff --git a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js index 243f7145f46d67..609d721b2b21d4 100644 --- a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js +++ b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js @@ -19,6 +19,7 @@ import {Button, StyleSheet, Text, View} from 'react-native'; import Performance from 'react-native/src/private/webapis/performance/Performance'; import PerformanceObserver, { type PerformanceEntry, + type PerformanceEventTiming, } from 'react-native/src/private/webapis/performance/PerformanceObserver'; const {useState, useCallback} = React; @@ -132,12 +133,13 @@ function PerformanceObserverUserTimingExample(): React.Node { {entries.map((entry, index) => entry.entryType === 'mark' ? ( - Mark {entry.name}: {entry.startTime} + Mark {entry.name}: {entry.startTime.toFixed(2)} ) : ( - Measure {entry.name}: {entry.startTime} -{' '} - {entry.startTime + entry.duration} ({entry.duration}ms) + Measure {entry.name}: {entry.startTime.toFixed(2)} -{' '} + {(entry.startTime + entry.duration).toFixed(2)} ( + {entry.duration.toFixed(2)}ms) ), )} @@ -146,6 +148,66 @@ function PerformanceObserverUserTimingExample(): React.Node { ); } +function PerformanceObserverEventTimingExample(): React.Node { + const theme = useContext(RNTesterThemeContext); + + const [count, setCount] = useState(0); + + const [entries, setEntries] = useState< + $ReadOnlyArray, + >([]); + + useEffect(() => { + const observer = new PerformanceObserver(list => { + const newEntries: $ReadOnlyArray = + // $FlowExpectedError[incompatible-type] This is guaranteed because we're only observing `event` entry types. + list.getEntries(); + setEntries(newEntries); + }); + + observer.observe({entryTypes: ['event']}); + + return () => observer.disconnect(); + }, []); + + const onPress = useCallback(() => { + busyWait(500); + // Force a state update to show how/if we're reporting paint times as well. + setCount(currentCount => currentCount + 1); + }, []); + + return ( + +