first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# 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)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_renderer_observers_events_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_renderer_observers_events OBJECT ${react_renderer_observers_events_SRC})
target_include_directories(react_renderer_observers_events PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_renderer_observers_events
react_debug
react_performance_timeline
react_timing
react_renderer_core
react_renderer_runtimescheduler
react_featureflags
react_renderer_uimanager
react_utils
rrc_view)
target_compile_reactnative_options(react_renderer_observers_events PRIVATE)
target_compile_options(react_renderer_observers_events PRIVATE -Wpedantic)

View File

@@ -0,0 +1,259 @@
/*
* 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 <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/timing/primitives.h>
#include <unordered_map>
namespace facebook::react {
namespace {
bool isTargetInRootShadowNode(
const SharedEventTarget& target,
const RootShadowNode::Shared& rootShadowNode) {
return target && rootShadowNode &&
target->getSurfaceId() == rootShadowNode->getSurfaceId();
}
bool hasPendingRenderingUpdates(
const SharedEventTarget& target,
const std::unordered_set<SurfaceId>&
surfaceIdsWithPendingRenderingUpdates) {
return target != nullptr &&
surfaceIdsWithPendingRenderingUpdates.contains(target->getSurfaceId());
}
struct StrKey {
size_t key;
StrKey(std::string_view s) : key(std::hash<std::string_view>{}(s)) {}
bool operator==(const StrKey& rhs) const {
return key == rhs.key;
}
};
struct StrKeyHash {
constexpr size_t operator()(const StrKey& strKey) const {
return 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<StrKey, std::string_view, StrKeyHash>;
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)
: performanceEntryReporter_(std::move(performanceEntryReporter)) {}
EventTag EventPerformanceLogger::onEventStart(
std::string_view name,
SharedEventTarget target,
std::optional<HighResTimeStamp> eventStartTimeStamp) {
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();
// The event start timestamp may be provided by the caller in order to
// specify the platform specific event start time.
HighResTimeStamp timeStamp =
eventStartTimeStamp ? *eventStartTimeStamp : HighResTimeStamp::now();
{
std::lock_guard lock(eventsInFlightMutex_);
eventsInFlight_.emplace(
eventTag,
EventEntry{
.name = reportedName, .target = target, .startTime = timeStamp});
}
return eventTag;
}
void EventPerformanceLogger::onEventProcessingStart(EventTag tag) {
auto performanceEntryReporter = performanceEntryReporter_.lock();
if (performanceEntryReporter == nullptr) {
return;
}
auto timeStamp = HighResTimeStamp::now();
{
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 = HighResTimeStamp::now();
{
std::lock_guard lock(eventsInFlightMutex_);
auto it = eventsInFlight_.find(tag);
if (it == eventsInFlight_.end()) {
return;
}
auto& entry = it->second;
react_native_assert(
entry.processingStartTime.has_value() &&
"Attempting to set processingEndTime while processingStartTime is not set.");
entry.processingEndTime = timeStamp;
}
}
void EventPerformanceLogger::dispatchPendingEventTimingEntries(
HighResTimeStamp taskEndTime,
const std::unordered_set<SurfaceId>&
surfaceIdsWithPendingRenderingUpdates) {
auto performanceEntryReporter = performanceEntryReporter_.lock();
if (performanceEntryReporter == nullptr) {
return;
}
std::lock_guard lock(eventsInFlightMutex_);
auto it = eventsInFlight_.begin();
while (it != eventsInFlight_.end()) {
auto& entry = it->second;
if (entry.isWaitingForDispatch() || entry.isWaitingForMount) {
++it;
} else if (hasPendingRenderingUpdates(
entry.target, surfaceIdsWithPendingRenderingUpdates)) {
// We'll wait for mount to report the event
entry.isWaitingForMount = true;
entry.taskEndTime = taskEndTime;
++it;
} else {
react_native_assert(
entry.processingStartTime.has_value() &&
"Attempted to report PerformanceEventTiming, which did not have processingStartTime defined.");
react_native_assert(
entry.processingEndTime.has_value() &&
"Attempted to report PerformanceEventTiming, which did not have processingEndTime defined.");
performanceEntryReporter->reportEvent(
std::string(entry.name),
entry.startTime,
taskEndTime - entry.startTime,
entry.processingStartTime.value(),
entry.processingEndTime.value(),
taskEndTime,
entry.interactionId);
it = eventsInFlight_.erase(it);
}
}
}
void EventPerformanceLogger::shadowTreeDidMount(
const RootShadowNode::Shared& rootShadowNode,
HighResTimeStamp mountTime) noexcept {
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.isWaitingForMount &&
isTargetInRootShadowNode(entry.target, rootShadowNode)) {
react_native_assert(
entry.processingStartTime.has_value() &&
"Attempted to report PerformanceEventTiming, which did not have processingStartTime defined.");
react_native_assert(
entry.processingEndTime.has_value() &&
"Attempted to report PerformanceEventTiming, which did not have processingEndTime defined.");
performanceEntryReporter->reportEvent(
std::string(entry.name),
entry.startTime,
mountTime - entry.startTime,
entry.processingStartTime.value(),
entry.processingEndTime.value(),
entry.taskEndTime.value(),
entry.interactionId);
it = eventsInFlight_.erase(it);
} else {
++it;
}
}
}
EventTag EventPerformanceLogger::createEventTag() {
sCurrentEventTag_++;
return sCurrentEventTag_;
}
} // namespace facebook::react

View File

@@ -0,0 +1,83 @@
/*
* 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 <react/performance/timeline/PerformanceEntryReporter.h>
#include <react/renderer/core/EventLogger.h>
#include <react/renderer/runtimescheduler/RuntimeSchedulerEventTimingDelegate.h>
#include <react/renderer/uimanager/UIManagerMountHook.h>
#include <memory>
#include <mutex>
#include <optional>
#include <string_view>
#include <unordered_map>
namespace facebook::react {
class EventPerformanceLogger : public EventLogger,
public RuntimeSchedulerEventTimingDelegate,
public UIManagerMountHook {
public:
explicit EventPerformanceLogger(std::weak_ptr<PerformanceEntryReporter> performanceEntryReporter);
#pragma mark - EventLogger
EventTag onEventStart(
std::string_view name,
SharedEventTarget target,
std::optional<HighResTimeStamp> eventStartTimeStamp = std::nullopt) override;
void onEventProcessingStart(EventTag tag) override;
void onEventProcessingEnd(EventTag tag) override;
#pragma mark - RuntimeSchedulerEventTimingDelegate
void dispatchPendingEventTimingEntries(
HighResTimeStamp taskEndTime,
const std::unordered_set<SurfaceId> &surfaceIdsWithPendingRenderingUpdates) override;
#pragma mark - UIManagerMountHook
void shadowTreeDidMount(const RootShadowNode::Shared &rootShadowNode, HighResTimeStamp mountTime) noexcept override;
private:
struct EventEntry {
std::string_view name;
SharedEventTarget target{nullptr};
HighResTimeStamp startTime;
std::optional<HighResTimeStamp> processingStartTime;
std::optional<HighResTimeStamp> processingEndTime;
std::optional<HighResTimeStamp> taskEndTime;
bool isWaitingForMount{false};
// TODO: Define the way to assign interaction IDs to the event chains
// (T141358175)
PerformanceEntryInteractionId interactionId{0};
bool isWaitingForDispatch()
{
return !processingEndTime.has_value();
}
};
// 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<EventTag, EventEntry> eventsInFlight_;
std::mutex eventsInFlightMutex_;
std::weak_ptr<PerformanceEntryReporter> performanceEntryReporter_;
EventTag sCurrentEventTag_{EMPTY_EVENT_TAG};
EventTag createEventTag();
};
} // namespace facebook::react

View File

@@ -0,0 +1,28 @@
# 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)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_renderer_observers_intersection_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_renderer_observers_intersection OBJECT ${react_renderer_observers_intersection_SRC})
target_include_directories(react_renderer_observers_intersection PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_renderer_observers_intersection
react_cxxreact
react_bridging
react_debug
react_renderer_core
react_renderer_graphics
react_renderer_mounting
react_renderer_runtimescheduler
react_renderer_uimanager
rrc_view
)
target_compile_reactnative_options(react_renderer_observers_intersection PRIVATE)
target_compile_options(react_renderer_observers_intersection PRIVATE -Wpedantic)

View File

@@ -0,0 +1,390 @@
/*
* 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 "IntersectionObserver.h"
#include <react/debug/react_native_assert.h>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/LayoutableShadowNode.h>
#include <react/renderer/core/ShadowNodeFamily.h>
#include <react/renderer/css/CSSLength.h>
#include <react/renderer/css/CSSPercentage.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/renderer/graphics/RectangleEdges.h>
#include <utility>
namespace facebook::react {
// Parse a normalized rootMargin string that's always in the format:
// "top right bottom left" where each value is either "Npx" or "N%"
// The string is already validated and normalized by JS.
// Returns a vector of 4 MarginValue structures (top, right, bottom, left).
// Returns an empty vector if parsing fails.
std::vector<MarginValue> parseNormalizedRootMargin(
const std::string& marginStr) {
// If unset or set to default, return empty so we don't apply any margins
if (marginStr.empty() || marginStr == "0px 0px 0px 0px") {
return {};
}
std::vector<MarginValue> values;
CSSSyntaxParser syntaxParser(marginStr);
// Parse exactly 4 space-separated values
while (!syntaxParser.isFinished() && values.size() < 4) {
syntaxParser.consumeWhitespace();
if (syntaxParser.isFinished()) {
break;
}
auto parsed = parseNextCSSValue<CSSLength, CSSPercentage>(syntaxParser);
if (std::holds_alternative<CSSLength>(parsed)) {
auto length = std::get<CSSLength>(parsed);
// Only support px units for rootMargin (per W3C spec)
if (length.unit != CSSLengthUnit::Px) {
return {};
}
values.push_back({length.value, false});
} else if (std::holds_alternative<CSSPercentage>(parsed)) {
auto percentage = std::get<CSSPercentage>(parsed);
values.push_back({percentage.value, true});
} else {
// Invalid token
return {};
}
}
// Should have exactly 4 values since JS normalizes to this format
if (values.size() != 4) {
return {};
}
return values;
}
namespace {
// Convert margin values to actual pixel values based on root rect.
// Per W3C spec: top/bottom percentages use height, left/right use width.
EdgeInsets calculateRootMarginInsets(
const std::vector<MarginValue>& margins,
const Rect& rootRect) {
Float top = margins[0].isPercentage
? (margins[0].value / 100.0f) * rootRect.size.height
: margins[0].value;
Float right = margins[1].isPercentage
? (margins[1].value / 100.0f) * rootRect.size.width
: margins[1].value;
Float bottom = margins[2].isPercentage
? (margins[2].value / 100.0f) * rootRect.size.height
: margins[2].value;
Float left = margins[3].isPercentage
? (margins[3].value / 100.0f) * rootRect.size.width
: margins[3].value;
return EdgeInsets{left, top, right, bottom};
}
} // namespace
IntersectionObserver::IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
ShadowNodeFamily::Shared targetShadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::vector<MarginValue> rootMargins)
: intersectionObserverId_(intersectionObserverId),
observationRootShadowNodeFamily_(
std::move(observationRootShadowNodeFamily)),
targetShadowNodeFamily_(std::move(targetShadowNodeFamily)),
thresholds_(std::move(thresholds)),
rootThresholds_(std::move(rootThresholds)),
rootMargins_(std::move(rootMargins)) {}
static std::shared_ptr<const ShadowNode> getShadowNode(
const ShadowNodeFamily::AncestorList& ancestors) {
if (ancestors.empty()) {
return nullptr;
}
const auto& lastAncestor = ancestors.back();
const ShadowNode& parentNode = lastAncestor.first.get();
int childIndex = lastAncestor.second;
const std::shared_ptr<const ShadowNode>& childNode =
parentNode.getChildren().at(childIndex);
return childNode;
}
static Rect getRootNodeBoundingRect(const RootShadowNode& rootShadowNode) {
const auto layoutableRootShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&rootShadowNode);
react_native_assert(
layoutableRootShadowNode != nullptr &&
"RootShadowNode instances must always inherit from LayoutableShadowNode.");
auto layoutMetrics = layoutableRootShadowNode->getLayoutMetrics();
if (layoutMetrics == EmptyLayoutMetrics ||
layoutMetrics.displayType == DisplayType::None) {
return Rect{};
}
// Apply the transform to translate the root view to its location in the
// viewport.
return layoutMetrics.frame * layoutableRootShadowNode->getTransform();
}
static Rect getBoundingRect(const ShadowNodeFamily::AncestorList& ancestors) {
auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics(
ancestors,
{/* .includeTransform = */ .includeTransform = true,
/* .includeViewportOffset = */ .includeViewportOffset = true});
return layoutMetrics == EmptyLayoutMetrics ? Rect{} : layoutMetrics.frame;
}
static Rect getClippedTargetBoundingRect(
const ShadowNodeFamily::AncestorList& targetAncestors) {
auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics(
targetAncestors,
{/* .includeTransform = */ .includeTransform = true,
/* .includeViewportOffset = */ .includeViewportOffset = true,
/* .applyParentClipping = */ .enableOverflowClipping = true});
return layoutMetrics == EmptyLayoutMetrics ? Rect{} : layoutMetrics.frame;
}
// Distinguishes between edge-adjacent vs. no intersection
static std::optional<Rect> intersectOrNull(
const Rect& rect1,
const Rect& rect2) {
auto result = Rect::intersect(rect1, rect2);
// Check if the result has zero area (could be empty or degenerate)
if (result.size.width == 0 || result.size.height == 0) {
// Check if origin is within both rectangles (touching case)
bool originInRect1 = result.origin.x >= rect1.getMinX() &&
result.origin.x <= rect1.getMaxX() &&
result.origin.y >= rect1.getMinY() &&
result.origin.y <= rect1.getMaxY();
bool originInRect2 = result.origin.x >= rect2.getMinX() &&
result.origin.x <= rect2.getMaxX() &&
result.origin.y >= rect2.getMinY() &&
result.origin.y <= rect2.getMaxY();
if (!originInRect1 || !originInRect2) {
// No actual intersection - rectangles are separated
return std::nullopt;
}
}
// Valid intersection (including degenerate edge/corner cases)
return result;
}
// Partially equivalent to
// https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
static std::optional<Rect> computeIntersection(
const Rect& rootBoundingRect,
const Rect& rootMarginBoundingRect,
const Rect& targetBoundingRect,
const ShadowNodeFamily::AncestorList& targetToRootAncestors,
bool hasExplicitRoot) {
// Use intersectOrNull to properly distinguish between edge-adjacent
// (valid intersection) and separated rectangles (no intersection)
auto absoluteIntersectionRect =
intersectOrNull(rootMarginBoundingRect, targetBoundingRect);
if (!absoluteIntersectionRect.has_value()) {
return std::nullopt;
}
// Coordinates of the target after clipping the parts hidden by a parent,
// until till the root (e.g.: in scroll views, or in views with a parent with
// overflow: hidden)
auto clippedTargetFromRoot =
getClippedTargetBoundingRect(targetToRootAncestors);
// Use root origin (without rootMargins) to translate coordinates of
// clippedTarget from relative to root, to top-level coordinate system
auto clippedTargetBoundingRect = hasExplicitRoot ? Rect{
.origin= rootBoundingRect.origin + clippedTargetFromRoot.origin,
.size=clippedTargetFromRoot.size}
: clippedTargetFromRoot;
return intersectOrNull(rootMarginBoundingRect, clippedTargetBoundingRect);
}
static Float getHighestThresholdCrossed(
Float intersectionRatio,
const std::vector<Float>& thresholds) {
Float highestThreshold = -1.0f;
for (auto threshold : thresholds) {
if (intersectionRatio >= threshold) {
highestThreshold = threshold;
}
}
return highestThreshold;
}
// Partially equivalent to
// https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
std::optional<IntersectionObserverEntry>
IntersectionObserver::updateIntersectionObservation(
const RootShadowNode& rootShadowNode,
HighResTimeStamp time) {
bool hasExplicitRoot = observationRootShadowNodeFamily_.has_value();
auto rootAncestors = hasExplicitRoot
? observationRootShadowNodeFamily_.value()->getAncestors(rootShadowNode)
: ShadowNodeFamily::AncestorList{};
// Absolute coordinates of the root
auto rootBoundingRect = hasExplicitRoot
? getBoundingRect(rootAncestors)
: getRootNodeBoundingRect(rootShadowNode);
auto rootMarginBoundingRect = rootBoundingRect;
// Apply rootMargin to expand/contract the root bounding rect
if (!rootMargins_.empty()) {
// Calculate the actual insets based on current root dimensions
// (converts percentages to pixels)
auto insets = calculateRootMarginInsets(rootMargins_, rootBoundingRect);
// Use outsetBy to expand the root rect (positive values expand, negative
// contract)
rootMarginBoundingRect = outsetBy(rootBoundingRect, insets);
}
auto targetAncestors = targetShadowNodeFamily_->getAncestors(rootShadowNode);
// Absolute coordinates of the target
auto targetBoundingRect = getBoundingRect(targetAncestors);
if ((hasExplicitRoot && rootAncestors.empty()) || targetAncestors.empty()) {
// If observation root or target is not a descendant of `rootShadowNode`
return setNotIntersectingState(
rootMarginBoundingRect, targetBoundingRect, {}, time);
}
auto targetToRootAncestors = hasExplicitRoot
? targetShadowNodeFamily_->getAncestors(*getShadowNode(rootAncestors))
: targetAncestors;
auto intersection = computeIntersection(
rootBoundingRect,
rootMarginBoundingRect,
targetBoundingRect,
targetToRootAncestors,
hasExplicitRoot);
auto intersectionRect =
intersection.has_value() ? intersection.value() : Rect{};
Float targetBoundingRectArea =
targetBoundingRect.size.width * targetBoundingRect.size.height;
auto intersectionRectArea =
intersectionRect.size.width * intersectionRect.size.height;
Float intersectionRatio =
targetBoundingRectArea == 0 // prevent division by zero
? 0
: intersectionRectArea / targetBoundingRectArea;
if (!intersection.has_value()) {
return setNotIntersectingState(
rootMarginBoundingRect, targetBoundingRect, intersectionRect, time);
}
auto highestThresholdCrossed =
getHighestThresholdCrossed(intersectionRatio, thresholds_);
auto highestRootThresholdCrossed = -1.0f;
if (rootThresholds_.has_value()) {
Float rootMarginBoundingRectArea =
rootMarginBoundingRect.size.width * rootMarginBoundingRect.size.height;
Float rootThresholdIntersectionRatio = rootMarginBoundingRectArea == 0
? 0
: intersectionRectArea / rootMarginBoundingRectArea;
highestRootThresholdCrossed = getHighestThresholdCrossed(
rootThresholdIntersectionRatio, rootThresholds_.value());
}
if (highestThresholdCrossed == -1.0f &&
highestRootThresholdCrossed == -1.0f) {
return setNotIntersectingState(
rootMarginBoundingRect, targetBoundingRect, intersectionRect, time);
}
return setIntersectingState(
rootMarginBoundingRect,
targetBoundingRect,
intersectionRect,
highestThresholdCrossed,
highestRootThresholdCrossed,
time);
}
std::optional<IntersectionObserverEntry>
IntersectionObserver::updateIntersectionObservationForSurfaceUnmount(
HighResTimeStamp time) {
return setNotIntersectingState(Rect{}, Rect{}, Rect{}, time);
}
std::optional<IntersectionObserverEntry>
IntersectionObserver::setIntersectingState(
const Rect& rootBoundingRect,
const Rect& targetBoundingRect,
const Rect& intersectionRect,
Float threshold,
Float rootThreshold,
HighResTimeStamp time) {
auto newState =
IntersectionObserverState::Intersecting(threshold, rootThreshold);
if (state_ != newState) {
state_ = newState;
IntersectionObserverEntry entry{
.intersectionObserverId = intersectionObserverId_,
.shadowNodeFamily = targetShadowNodeFamily_,
.targetRect = targetBoundingRect,
.rootRect = rootBoundingRect,
.intersectionRect = intersectionRect,
.isIntersectingAboveThresholds = true,
.time = time,
};
return std::optional<IntersectionObserverEntry>{std::move(entry)};
}
return std::nullopt;
}
std::optional<IntersectionObserverEntry>
IntersectionObserver::setNotIntersectingState(
const Rect& rootBoundingRect,
const Rect& targetBoundingRect,
const Rect& intersectionRect,
HighResTimeStamp time) {
if (state_ != IntersectionObserverState::NotIntersecting()) {
state_ = IntersectionObserverState::NotIntersecting();
IntersectionObserverEntry entry{
.intersectionObserverId = intersectionObserverId_,
.shadowNodeFamily = targetShadowNodeFamily_,
.targetRect = targetBoundingRect,
.rootRect = rootBoundingRect,
.intersectionRect = intersectionRect,
.isIntersectingAboveThresholds = false,
.time = time,
};
return std::optional<IntersectionObserverEntry>(std::move(entry));
}
return std::nullopt;
}
} // namespace facebook::react

View File

@@ -0,0 +1,106 @@
/*
* 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 <react/renderer/components/root/RootShadowNode.h>
#include <react/renderer/core/ShadowNodeFamily.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/graphics/Rect.h>
#include <memory>
#include "IntersectionObserverState.h"
namespace facebook::react {
using IntersectionObserverObserverId = int32_t;
// Structure to hold a margin value that can be either pixels or percentage
struct MarginValue {
Float value;
bool isPercentage;
};
// Parse a normalized rootMargin string that's always in the format:
// "top right bottom left" where each value is either "Npx" or "N%"
// The string is already validated and normalized by JS.
// Returns a vector of 4 MarginValue structures (top, right, bottom, left).
std::vector<MarginValue> parseNormalizedRootMargin(const std::string &marginStr);
struct IntersectionObserverEntry {
IntersectionObserverObserverId intersectionObserverId;
ShadowNodeFamily::Shared shadowNodeFamily;
Rect targetRect;
Rect rootRect;
Rect intersectionRect;
bool isIntersectingAboveThresholds;
HighResTimeStamp time;
bool sameShadowNodeFamily(const ShadowNodeFamily &otherShadowNodeFamily) const
{
return std::addressof(*shadowNodeFamily) == std::addressof(otherShadowNodeFamily);
}
};
class IntersectionObserver {
public:
IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
ShadowNodeFamily::Shared targetShadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::vector<MarginValue> rootMargins);
// Partially equivalent to
// https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
std::optional<IntersectionObserverEntry> updateIntersectionObservation(
const RootShadowNode &rootShadowNode,
HighResTimeStamp time);
std::optional<IntersectionObserverEntry> updateIntersectionObservationForSurfaceUnmount(HighResTimeStamp time);
IntersectionObserverObserverId getIntersectionObserverId() const
{
return intersectionObserverId_;
}
ShadowNodeFamily::Shared getTargetShadowNodeFamily() const
{
return targetShadowNodeFamily_;
}
std::vector<Float> getThresholds() const
{
return thresholds_;
}
private:
std::optional<IntersectionObserverEntry> setIntersectingState(
const Rect &rootBoundingRect,
const Rect &targetBoundingRect,
const Rect &intersectionRect,
Float threshold,
Float rootThreshold,
HighResTimeStamp time);
std::optional<IntersectionObserverEntry> setNotIntersectingState(
const Rect &rootBoundingRect,
const Rect &targetBoundingRect,
const Rect &intersectionRect,
HighResTimeStamp time);
IntersectionObserverObserverId intersectionObserverId_;
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily_;
ShadowNodeFamily::Shared targetShadowNodeFamily_;
std::vector<Float> thresholds_;
std::optional<std::vector<Float>> rootThresholds_;
// Parsed and expanded rootMargin values (top, right, bottom, left)
std::vector<MarginValue> rootMargins_;
mutable IntersectionObserverState state_ = IntersectionObserverState::Initial();
};
} // namespace facebook::react

View File

@@ -0,0 +1,336 @@
/*
* 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 "IntersectionObserverManager.h"
#include <cxxreact/JSExecutor.h>
#include <cxxreact/TraceSection.h>
#include <react/debug/react_native_assert.h>
#include <utility>
#include "IntersectionObserver.h"
namespace facebook::react {
namespace {
RootShadowNode::Shared getRootShadowNode(
SurfaceId surfaceId,
const ShadowTreeRegistry& shadowTreeRegistry,
std::unordered_map<SurfaceId, RootShadowNode::Shared>& cache) {
auto it = cache.find(surfaceId);
if (it == cache.end()) {
RootShadowNode::Shared rootShadowNode = nullptr;
shadowTreeRegistry.visit(surfaceId, [&](const ShadowTree& shadowTree) {
rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
cache.insert({surfaceId, rootShadowNode});
return rootShadowNode;
} else {
return it->second;
}
}
} // namespace
IntersectionObserverManager::IntersectionObserverManager() = default;
void IntersectionObserverManager::observe(
IntersectionObserverObserverId intersectionObserverId,
const std::optional<ShadowNodeFamily::Shared>&
observationRootShadowNodeFamily,
const ShadowNodeFamily::Shared& shadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::optional<std::string> rootMargin,
const UIManager& /*uiManager*/) {
TraceSection s("IntersectionObserverManager::observe");
auto surfaceId = shadowNodeFamily->getSurfaceId();
// Register observer
std::unique_lock lock(observersMutex_);
auto& observers = observersBySurfaceId_[surfaceId];
// Parse rootMargin string into MarginValue structures
// Default to "0px 0px 0px 0px" if not provided
auto parsedRootMargin =
parseNormalizedRootMargin(rootMargin.value_or("0px 0px 0px 0px"));
observers.emplace_back(
std::make_unique<IntersectionObserver>(
intersectionObserverId,
observationRootShadowNodeFamily,
shadowNodeFamily,
std::move(thresholds),
std::move(rootThresholds),
std::move(parsedRootMargin)));
observersPendingInitialization_.emplace_back(observers.back().get());
}
void IntersectionObserverManager::unobserve(
IntersectionObserverObserverId intersectionObserverId,
const ShadowNodeFamily::Shared& shadowNodeFamily) {
TraceSection s("IntersectionObserverManager::unobserve");
// This doesn't need to be protected by the mutex because it is only
// accessed and modified from the JS thread.
observersPendingInitialization_.erase(
std::remove_if(
observersPendingInitialization_.begin(),
observersPendingInitialization_.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& observer) {
return (
observer->getIntersectionObserverId() ==
intersectionObserverId &&
observer->getTargetShadowNodeFamily() == shadowNodeFamily);
}),
observersPendingInitialization_.end());
{
std::unique_lock lock(observersMutex_);
auto surfaceId = shadowNodeFamily->getSurfaceId();
auto observersIt = observersBySurfaceId_.find(surfaceId);
if (observersIt == observersBySurfaceId_.end()) {
return;
}
auto& observers = observersIt->second;
observers.erase(
std::remove_if(
observers.begin(),
observers.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& observer) {
return observer->getIntersectionObserverId() ==
intersectionObserverId &&
observer->getTargetShadowNodeFamily() == shadowNodeFamily;
}),
observers.end());
if (observers.empty()) {
observersBySurfaceId_.erase(surfaceId);
}
}
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.erase(
std::remove_if(
pendingEntries_.begin(),
pendingEntries_.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& entry) {
return entry.intersectionObserverId == intersectionObserverId &&
entry.sameShadowNodeFamily(*shadowNodeFamily);
}),
pendingEntries_.end());
}
}
void IntersectionObserverManager::connect(
RuntimeScheduler& runtimeScheduler,
UIManager& uiManager,
std::function<void()> notifyIntersectionObserversCallback) {
TraceSection s("IntersectionObserverManager::connect");
notifyIntersectionObserversCallback_ =
std::move(notifyIntersectionObserversCallback);
// Fail-safe in case the caller doesn't guarantee consistency.
if (mountHookRegistered_) {
return;
}
runtimeScheduler.setIntersectionObserverDelegate(this);
uiManager.registerMountHook(*this);
shadowTreeRegistry_ = &uiManager.getShadowTreeRegistry();
mountHookRegistered_ = true;
}
void IntersectionObserverManager::disconnect(
RuntimeScheduler& runtimeScheduler,
UIManager& uiManager) {
TraceSection s("IntersectionObserverManager::disconnect");
// Fail-safe in case the caller doesn't guarantee consistency.
if (!mountHookRegistered_) {
return;
}
runtimeScheduler.setIntersectionObserverDelegate(nullptr);
uiManager.unregisterMountHook(*this);
shadowTreeRegistry_ = nullptr;
mountHookRegistered_ = false;
notifyIntersectionObserversCallback_ = nullptr;
}
std::vector<IntersectionObserverEntry>
IntersectionObserverManager::takeRecords() {
std::unique_lock lock(pendingEntriesMutex_);
notifiedIntersectionObservers_ = false;
std::vector<IntersectionObserverEntry> entries;
pendingEntries_.swap(entries);
return entries;
}
#pragma mark - RuntimeSchedulerIntersectionObserverDelegate
void IntersectionObserverManager::updateIntersectionObservations(
const std::unordered_set<SurfaceId>&
surfaceIdsWithPendingRenderingUpdates) {
// On Web, this step invoked from the Event Loop computes the intersections
// and schedules the notifications.
// Doing exactly the same in React Native would exclude the time it takes
// to process these transactions and mount the operations in the host
// platform, which would provide inaccurate timings for measuring paint time.
// Instead, we use mount hooks to compute this.
// What we can use this step for is to dispatch initial notifications for
// observers that were just set up, but for which we do not have any pending
// transactions that would trigger the mount hooks.
// In those cases it is ok to dispatch the notifications now, because the
// current state is already accurate.
if (observersPendingInitialization_.empty()) {
return;
}
TraceSection s(
"IntersectionObserverManager::updateIntersectionObservations",
"pendingObserverCount",
observersPendingInitialization_.size());
std::unordered_map<SurfaceId, RootShadowNode::Shared> rootShadowNodeCache;
for (auto observer : observersPendingInitialization_) {
auto surfaceId = observer->getTargetShadowNodeFamily()->getSurfaceId();
// If there are pending updates, we just wait for the mount hook.
if (surfaceIdsWithPendingRenderingUpdates.contains(surfaceId)) {
continue;
}
RootShadowNode::Shared rootShadowNode =
getRootShadowNode(surfaceId, *shadowTreeRegistry_, rootShadowNodeCache);
// If the surface doesn't exist for some reason, we skip initial
// notification.
if (!rootShadowNode) {
continue;
}
auto entry = observer->updateIntersectionObservation(
*rootShadowNode, HighResTimeStamp::now());
if (entry) {
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.push_back(std::move(entry).value());
}
notifyObserversIfNecessary();
}
}
observersPendingInitialization_.clear();
}
#pragma mark - UIManagerMountHook
void IntersectionObserverManager::shadowTreeDidMount(
const RootShadowNode::Shared& rootShadowNode,
HighResTimeStamp time) noexcept {
TraceSection s("IntersectionObserverManager::shadowTreeDidMount");
updateIntersectionObservations(
rootShadowNode->getSurfaceId(), rootShadowNode.get(), time);
}
void IntersectionObserverManager::shadowTreeDidUnmount(
SurfaceId surfaceId,
HighResTimeStamp time) noexcept {
TraceSection s("IntersectionObserverManager::shadowTreeDidUnmount");
updateIntersectionObservations(surfaceId, nullptr, time);
}
#pragma mark - Private methods
void IntersectionObserverManager::updateIntersectionObservations(
SurfaceId surfaceId,
const RootShadowNode* rootShadowNode,
HighResTimeStamp time) {
std::vector<IntersectionObserverEntry> entries;
// Run intersection observations
{
std::shared_lock lock(observersMutex_);
auto observersIt = observersBySurfaceId_.find(surfaceId);
if (observersIt == observersBySurfaceId_.end()) {
return;
}
TraceSection s(
"IntersectionObserverManager::updateIntersectionObservations(mount)",
"observerCount",
observersIt->second.size());
auto& observers = observersIt->second;
for (auto& observer : observers) {
std::optional<IntersectionObserverEntry> entry;
if (rootShadowNode != nullptr) {
entry = observer->updateIntersectionObservation(*rootShadowNode, time);
} else {
entry = observer->updateIntersectionObservationForSurfaceUnmount(time);
}
if (entry) {
entries.push_back(std::move(entry).value());
}
}
}
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.insert(
pendingEntries_.end(), entries.begin(), entries.end());
}
notifyObserversIfNecessary();
}
/**
* This method allows us to avoid scheduling multiple calls to notify observers
* in the JS thread. We schedule one and skip subsequent ones (we just append
* the entries to the pending list and wait for the scheduled task to consume
* all of them).
*/
void IntersectionObserverManager::notifyObserversIfNecessary() {
bool dispatchNotification = false;
{
std::unique_lock lock(pendingEntriesMutex_);
if (!pendingEntries_.empty() && !notifiedIntersectionObservers_) {
notifiedIntersectionObservers_ = true;
dispatchNotification = true;
}
}
if (dispatchNotification) {
notifyObservers();
}
}
void IntersectionObserverManager::notifyObservers() {
TraceSection s("IntersectionObserverManager::notifyObservers");
notifyIntersectionObserversCallback_();
}
} // namespace facebook::react

View File

@@ -0,0 +1,92 @@
/*
* 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 <react/renderer/core/ShadowNode.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
#include <react/renderer/runtimescheduler/RuntimeSchedulerIntersectionObserverDelegate.h>
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/UIManagerMountHook.h>
#include <memory>
#include <vector>
#include "IntersectionObserver.h"
namespace facebook::react {
class IntersectionObserverManager final : public UIManagerMountHook,
public RuntimeSchedulerIntersectionObserverDelegate {
public:
IntersectionObserverManager();
void observe(
IntersectionObserverObserverId intersectionObserverId,
const std::optional<ShadowNodeFamily::Shared> &observationRootShadowNode,
const ShadowNodeFamily::Shared &shadowNode,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::optional<std::string> rootMargin,
const UIManager &uiManager);
void unobserve(IntersectionObserverObserverId intersectionObserverId, const ShadowNodeFamily::Shared &shadowNode);
void connect(
RuntimeScheduler &runtimeScheduler,
UIManager &uiManager,
std::function<void()> notifyIntersectionObserversCallback);
void disconnect(RuntimeScheduler &runtimeScheduler, UIManager &uiManager);
std::vector<IntersectionObserverEntry> takeRecords();
#pragma mark - RuntimeSchedulerIntersectionObserverDelegate
void updateIntersectionObservations(
const std::unordered_set<SurfaceId> &surfaceIdsWithPendingRenderingUpdates) override;
#pragma mark - UIManagerMountHook
void shadowTreeDidMount(const RootShadowNode::Shared &rootShadowNode, HighResTimeStamp time) noexcept override;
void shadowTreeDidUnmount(SurfaceId surfaceId, HighResTimeStamp time) noexcept override;
private:
mutable std::unordered_map<SurfaceId, std::vector<std::unique_ptr<IntersectionObserver>>> observersBySurfaceId_;
mutable std::shared_mutex observersMutex_;
// This is defined as a list of pointers to keep the ownership of the
// observers in the map.
std::vector<IntersectionObserver *> observersPendingInitialization_;
// This is only accessed from the JS thread at the end of the event loop tick,
// so it is safe to retain it as a raw pointer.
// We need to retain it here because the RuntimeScheduler does not provide
// it when calling `updateIntersectionObservations`.
const ShadowTreeRegistry *shadowTreeRegistry_{nullptr};
mutable std::function<void()> notifyIntersectionObserversCallback_;
mutable std::vector<IntersectionObserverEntry> pendingEntries_;
mutable std::mutex pendingEntriesMutex_;
mutable bool notifiedIntersectionObservers_{};
mutable bool mountHookRegistered_{};
void notifyObserversIfNecessary();
void notifyObservers();
// Equivalent to
// https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
void updateIntersectionObservations(SurfaceId surfaceId, const RootShadowNode *rootShadowNode, HighResTimeStamp time);
const IntersectionObserver &getRegisteredIntersectionObserver(
SurfaceId surfaceId,
IntersectionObserverObserverId observerId) const;
};
} // namespace facebook::react

View File

@@ -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.
*/
#include "IntersectionObserverState.h"
namespace facebook::react {
IntersectionObserverState IntersectionObserverState::Initial() {
static const IntersectionObserverState state =
IntersectionObserverState(IntersectionObserverStateType::Initial);
return state;
}
IntersectionObserverState IntersectionObserverState::NotIntersecting() {
static const IntersectionObserverState state =
IntersectionObserverState(IntersectionObserverStateType::NotIntersecting);
return state;
}
IntersectionObserverState IntersectionObserverState::Intersecting(
Float threshold,
Float rootThreshold) {
return {
IntersectionObserverStateType::Intersecting, threshold, rootThreshold};
}
IntersectionObserverState::IntersectionObserverState(
IntersectionObserverStateType state)
: state_(state) {}
IntersectionObserverState::IntersectionObserverState(
IntersectionObserverStateType state,
Float threshold,
Float rootThreshold)
: state_(state), threshold_(threshold), rootThreshold_(rootThreshold) {}
bool IntersectionObserverState::isIntersecting() const {
return state_ == IntersectionObserverStateType::Intersecting;
}
bool IntersectionObserverState::operator==(
const IntersectionObserverState& other) const {
if (state_ != other.state_) {
return false;
}
if (state_ != IntersectionObserverStateType::Intersecting) {
return true;
}
return threshold_ == other.threshold_ &&
rootThreshold_ == other.rootThreshold_;
}
bool IntersectionObserverState::operator!=(
const IntersectionObserverState& other) const {
return !(*this == other);
}
} // namespace facebook::react

View File

@@ -0,0 +1,51 @@
/*
* 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 <react/renderer/graphics/Float.h>
#include <optional>
namespace {
enum class IntersectionObserverStateType {
Initial,
NotIntersecting,
Intersecting,
};
}
namespace facebook::react {
class IntersectionObserverState {
public:
static IntersectionObserverState Initial();
static IntersectionObserverState NotIntersecting();
static IntersectionObserverState Intersecting(Float threshold);
static IntersectionObserverState Intersecting(Float threshold, Float rootThreshold);
bool isIntersecting() const;
bool operator==(const IntersectionObserverState &other) const;
bool operator!=(const IntersectionObserverState &other) const;
private:
explicit IntersectionObserverState(IntersectionObserverStateType state);
IntersectionObserverState(IntersectionObserverStateType state, Float threshold, Float rootThreshold);
IntersectionObserverStateType state_;
// This value is only relevant if the state is
// IntersectionObserverStateType::Intersecting.
Float threshold_{};
// This value is only relevant if the state is
// IntersectionObserverStateType::Intersecting.
std::optional<Float> rootThreshold_{};
};
} // namespace facebook::react

View File

@@ -0,0 +1,24 @@
# 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)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_renderer_observers_mutation_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_renderer_observers_mutation OBJECT ${react_renderer_observers_mutation_SRC})
target_include_directories(react_renderer_observers_mutation PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_renderer_observers_mutation
react_cxxreact
react_renderer_core
react_renderer_uimanager
react_renderer_mounting
react_bridging
)
target_compile_reactnative_options(react_renderer_observers_mutation PRIVATE)
target_compile_options(react_renderer_observers_mutation PRIVATE -Wpedantic)

View File

@@ -0,0 +1,187 @@
/*
* 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 "MutationObserver.h"
#include <react/renderer/core/ShadowNodeTraits.h>
#include <react/renderer/uimanager/primitives.h>
namespace facebook::react {
MutationObserver::MutationObserver(MutationObserverId mutationObserverId)
: mutationObserverId_(mutationObserverId) {}
void MutationObserver::observe(
std::shared_ptr<const ShadowNodeFamily> targetShadowNodeFamily,
bool observeSubtree) {
auto& list = observeSubtree ? deeplyObservedShadowNodeFamilies_
: shallowlyObservedShadowNodeFamilies_;
auto& otherList = observeSubtree ? shallowlyObservedShadowNodeFamilies_
: deeplyObservedShadowNodeFamilies_;
if (std::find(list.begin(), list.end(), targetShadowNodeFamily) !=
list.end()) {
// It is already being observed correctly.
return;
}
auto it =
std::find(otherList.begin(), otherList.end(), targetShadowNodeFamily);
if (it != otherList.end()) {
// It is being observed incorrectly.
otherList.erase(it);
}
list.push_back(targetShadowNodeFamily);
}
static std::shared_ptr<const ShadowNode> getShadowNodeInTree(
const ShadowNodeFamily& shadowNodeFamily,
const ShadowNode& newTree) {
auto ancestors = shadowNodeFamily.getAncestors(newTree);
if (ancestors.empty()) {
return nullptr;
}
auto pair = ancestors.rbegin();
return pair->first.get().getChildren().at(pair->second);
}
static std::shared_ptr<const ShadowNode> findNodeOfSameFamily(
const std::vector<std::shared_ptr<const ShadowNode>>& list,
const ShadowNode& node) {
for (auto& current : list) {
if (ShadowNode::sameFamily(node, *current)) {
return current;
}
}
return nullptr;
}
void MutationObserver::recordMutations(
const RootShadowNode& oldRootShadowNode,
const RootShadowNode& newRootShadowNode,
std::vector<MutationRecord>& recordedMutations) const {
// This tracks the nodes that have already been processed by this observer,
// so we avoid unnecessary work and duplicated entries.
SetOfShadowNodePointers processedNodes;
// We go over the deeply observed nodes first to avoid skipping nodes that
// have only been checked shallowly.
for (const auto& targetShadowNodeFamily : deeplyObservedShadowNodeFamilies_) {
recordMutationsInTarget(
*targetShadowNodeFamily,
oldRootShadowNode,
newRootShadowNode,
true,
recordedMutations,
processedNodes);
}
for (const auto& targetShadowNodeFamily :
shallowlyObservedShadowNodeFamilies_) {
recordMutationsInTarget(
*targetShadowNodeFamily,
oldRootShadowNode,
newRootShadowNode,
false,
recordedMutations,
processedNodes);
}
}
void MutationObserver::recordMutationsInTarget(
const ShadowNodeFamily& targetShadowNodeFamily,
const RootShadowNode& oldRootShadowNode,
const RootShadowNode& newRootShadowNode,
bool observeSubtree,
std::vector<MutationRecord>& recordedMutations,
SetOfShadowNodePointers& processedNodes) const {
// If the node isnt't present in the old tree, it's either:
// - A new node. In that case, the mutation happened in its parent, not in the
// node itself.
// - A non-existent node. In that case, there are no new mutations.
auto oldTargetShadowNode =
getShadowNodeInTree(targetShadowNodeFamily, oldRootShadowNode);
if (!oldTargetShadowNode) {
return;
}
// If the node isn't present in the new tree (and we didn't return in the
// previous check), it means the whole node was removed. In that case we don't
// record any mutations in the node itself (maybe in its parent if there are
// other observers set up).
auto newTargetShadowNode =
getShadowNodeInTree(targetShadowNodeFamily, newRootShadowNode);
if (!newTargetShadowNode) {
return;
}
recordMutationsInSubtrees(
oldTargetShadowNode,
newTargetShadowNode,
observeSubtree,
recordedMutations,
processedNodes);
}
void MutationObserver::recordMutationsInSubtrees(
const std::shared_ptr<const ShadowNode>& oldNode,
const std::shared_ptr<const ShadowNode>& newNode,
bool observeSubtree,
std::vector<MutationRecord>& recordedMutations,
SetOfShadowNodePointers& processedNodes) const {
bool isSameNode = oldNode.get() == newNode.get();
// If the nodes are referentially equal, their children are also the same.
if (isSameNode ||
processedNodes.find(oldNode.get()) != processedNodes.end()) {
return;
}
processedNodes.insert(oldNode.get());
auto oldChildren = oldNode->getChildren();
auto newChildren = newNode->getChildren();
std::vector<std::shared_ptr<const ShadowNode>> addedNodes;
std::vector<std::shared_ptr<const ShadowNode>> removedNodes;
// Check for removed nodes (and equal nodes for further inspection)
for (auto& oldChild : oldChildren) {
auto newChild = findNodeOfSameFamily(newChildren, *oldChild);
if (!newChild) {
removedNodes.push_back(oldChild);
} else if (observeSubtree) {
// Nodes are present in both tress. If `subtree` is set to true,
// we continue checking their children.
recordMutationsInSubtrees(
oldChild,
newChild,
observeSubtree,
recordedMutations,
processedNodes);
}
}
// Check for added nodes
for (auto& newChild : newChildren) {
auto oldChild = findNodeOfSameFamily(oldChildren, *newChild);
if (!oldChild) {
addedNodes.push_back(newChild);
}
}
if (!addedNodes.empty() || !removedNodes.empty()) {
recordedMutations.emplace_back(
MutationRecord{
.mutationObserverId = mutationObserverId_,
.targetShadowNode = oldNode,
.addedShadowNodes = std::move(addedNodes),
.removedShadowNodes = std::move(removedNodes)});
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,71 @@
/*
* 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 <react/renderer/components/root/RootShadowNode.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/core/ShadowNodeFamily.h>
namespace facebook::react {
using MutationObserverId = int32_t;
struct MutationRecord {
MutationObserverId mutationObserverId;
std::shared_ptr<const ShadowNode> targetShadowNode;
std::vector<std::shared_ptr<const ShadowNode>> addedShadowNodes;
std::vector<std::shared_ptr<const ShadowNode>> removedShadowNodes;
};
class MutationObserver {
public:
explicit MutationObserver(MutationObserverId mutationObserverId);
// delete copy constructor
MutationObserver(const MutationObserver &) = delete;
// delete copy assignment
MutationObserver &operator=(const MutationObserver &) = delete;
// allow move constructor
MutationObserver(MutationObserver &&) = default;
// allow move assignment
MutationObserver &operator=(MutationObserver &&) = default;
void observe(std::shared_ptr<const ShadowNodeFamily> targetShadowNodeFamily, bool observeSubtree);
void recordMutations(
const RootShadowNode &oldRootShadowNode,
const RootShadowNode &newRootShadowNode,
std::vector<MutationRecord> &recordedMutations) const;
private:
MutationObserverId mutationObserverId_;
std::vector<std::shared_ptr<const ShadowNodeFamily>> deeplyObservedShadowNodeFamilies_;
std::vector<std::shared_ptr<const ShadowNodeFamily>> shallowlyObservedShadowNodeFamilies_;
using SetOfShadowNodePointers = std::unordered_set<const ShadowNode *>;
void recordMutationsInTarget(
const ShadowNodeFamily &targetShadowNodeFamily,
const RootShadowNode &oldRootShadowNode,
const RootShadowNode &newRootShadowNode,
bool observeSubtree,
std::vector<MutationRecord> &recordedMutations,
SetOfShadowNodePointers &processedNodes) const;
void recordMutationsInSubtrees(
const std::shared_ptr<const ShadowNode> &oldNode,
const std::shared_ptr<const ShadowNode> &newNode,
bool observeSubtree,
std::vector<MutationRecord> &recordedMutations,
SetOfShadowNodePointers &processedNodes) const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,128 @@
/*
* 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 "MutationObserverManager.h"
#include <cxxreact/TraceSection.h>
#include <utility>
#include "MutationObserver.h"
namespace facebook::react {
MutationObserverManager::MutationObserverManager() = default;
void MutationObserverManager::observe(
MutationObserverId mutationObserverId,
std::shared_ptr<const ShadowNode> shadowNode,
bool observeSubtree,
const UIManager& /*uiManager*/) {
TraceSection s("MutationObserverManager::observe");
auto surfaceId = shadowNode->getSurfaceId();
auto shadowNodeFamily = shadowNode->getFamilyShared();
auto& observers = observersBySurfaceId_[surfaceId];
auto observerIt = observers.find(mutationObserverId);
if (observerIt == observers.end()) {
auto observer = MutationObserver{mutationObserverId};
observer.observe(shadowNodeFamily, observeSubtree);
observers.emplace(mutationObserverId, std::move(observer));
} else {
auto& observer = observerIt->second;
observer.observe(shadowNodeFamily, observeSubtree);
}
}
void MutationObserverManager::unobserveAll(
MutationObserverId mutationObserverId) {
TraceSection s("MutationObserverManager::unobserveAll");
for (auto it = observersBySurfaceId_.begin();
it != observersBySurfaceId_.end();) {
auto& observers = it->second;
auto deleted = observers.erase(mutationObserverId);
if (deleted > 0 && observers.empty()) {
it = observersBySurfaceId_.erase(it);
} else {
++it;
}
}
}
void MutationObserverManager::connect(
UIManager& uiManager,
OnMutations&& onMutations) {
TraceSection s("MutationObserverManager::connect");
// Fail-safe in case the caller doesn't guarantee consistency.
if (commitHookRegistered_) {
return;
}
onMutations_ = std::move(onMutations);
uiManager.registerCommitHook(*this);
commitHookRegistered_ = true;
}
void MutationObserverManager::disconnect(UIManager& uiManager) {
TraceSection s("MutationObserverManager::disconnect");
// Fail-safe in case the caller doesn't guarantee consistency.
if (!commitHookRegistered_) {
return;
}
uiManager.unregisterCommitHook(*this);
onMutations_ = nullptr;
commitHookRegistered_ = false;
}
void MutationObserverManager::commitHookWasRegistered(
const UIManager& uiManager) noexcept {}
void MutationObserverManager::commitHookWasUnregistered(
const UIManager& uiManager) noexcept {}
RootShadowNode::Unshared MutationObserverManager::shadowTreeWillCommit(
const ShadowTree& shadowTree,
const RootShadowNode::Shared& oldRootShadowNode,
const RootShadowNode::Unshared& newRootShadowNode,
const ShadowTree::CommitOptions& commitOptions) noexcept {
if (commitOptions.source == ShadowTree::CommitSource::React) {
runMutationObservations(shadowTree, *oldRootShadowNode, *newRootShadowNode);
}
return newRootShadowNode;
}
void MutationObserverManager::runMutationObservations(
const ShadowTree& shadowTree,
const RootShadowNode& oldRootShadowNode,
const RootShadowNode& newRootShadowNode) {
TraceSection s("MutationObserverManager::runMutationObservations");
auto surfaceId = shadowTree.getSurfaceId();
auto observersIt = observersBySurfaceId_.find(surfaceId);
if (observersIt == observersBySurfaceId_.end()) {
return;
}
std::vector<MutationRecord> mutationRecords;
auto& observers = observersIt->second;
for (const auto& [mutationObserverId, observer] : observers) {
observer.recordMutations(
oldRootShadowNode, newRootShadowNode, mutationRecords);
}
if (!mutationRecords.empty()) {
onMutations_(mutationRecords);
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,60 @@
/*
* 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 <react/renderer/core/ShadowNode.h>
#include <react/renderer/mounting/ShadowTree.h>
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/UIManagerCommitHook.h>
#include <vector>
#include "MutationObserver.h"
namespace facebook::react {
using OnMutations = std::function<void(std::vector<MutationRecord> &)>;
class MutationObserverManager final : public UIManagerCommitHook {
public:
MutationObserverManager();
void observe(
MutationObserverId mutationObserverId,
std::shared_ptr<const ShadowNode> shadowNode,
bool observeSubtree,
const UIManager &uiManager);
void unobserveAll(MutationObserverId mutationObserverId);
void connect(UIManager &uiManager, OnMutations &&onMutations);
void disconnect(UIManager &uiManager);
#pragma mark - UIManagerCommitHook
void commitHookWasRegistered(const UIManager &uiManager) noexcept override;
void commitHookWasUnregistered(const UIManager &uiManager) noexcept override;
RootShadowNode::Unshared shadowTreeWillCommit(
const ShadowTree &shadowTree,
const RootShadowNode::Shared &oldRootShadowNode,
const RootShadowNode::Unshared &newRootShadowNode,
const ShadowTree::CommitOptions &commitOptions) noexcept override;
private:
std::unordered_map<SurfaceId, std::unordered_map<MutationObserverId, MutationObserver>> observersBySurfaceId_;
OnMutations onMutations_;
bool commitHookRegistered_{};
void runMutationObservations(
const ShadowTree &shadowTree,
const RootShadowNode &oldRootShadowNode,
const RootShadowNode &newRootShadowNode);
};
} // namespace facebook::react