Perf/pointer event handling#9
Conversation
m_capturedPointers was a vector searched linearly with std::find() on every pointer move/press/release event at 60Hz. Changed to unordered_set for O(1) lookup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GetTouchableViewsInPathToRoot() walked from target to root on every pointer event. Added single-entry cache keyed by target tag to avoid redundant walks for consecutive events on the same component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR makes two performance improvements to pointer-event handling: (1) converts
Confidence Score: 4/5Safe to merge with low risk for most interaction patterns, but the cache's lack of tree-mutation invalidation is a correctness hazard that should be addressed before shipping. The unordered_set refactor is clean and correct. The cache optimisation is the right idea, but missing invalidation logic means stale paths can be returned after a Fabric tree commit that restructures ancestors of a still-live leaf node — a realistic scenario in dynamic React Native UIs. This warrants a P1 fix before merge. vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp — specifically the GetTouchableViewsInPathToRoot cache read/write and its interaction with Fabric tree commits. Important Files Changed
Sequence DiagramsequenceDiagram
participant OS as OS Input
participant CEH as CompositionEventHandler
participant Cache as m_cachedEventPath
participant Tree as ComponentView Tree
OS->>CEH: onPointerMoved(pointerPoint)
CEH->>CEH: getTargetPointerArgs() → tag
CEH->>CEH: HandleIncomingPointerEvent(targetView)
CEH->>Cache: GetTouchableViewsInPathToRoot(targetView)
alt cache hit (same tag)
Cache-->>CEH: return m_cachedEventPath (⚠ may be stale if tree mutated)
else cache miss
CEH->>Tree: walk parent chain
Tree-->>CEH: path[]
CEH->>Cache: store tag + path
Cache-->>CEH: return path
end
CEH->>CEH: fire enter/leave events along path
OS->>CEH: onPointerReleased(pointerPoint)
CEH->>CEH: DispatchTouchEvent(End)
CEH->>CEH: IsPointerWithinInitialTree(activeTouch)
CEH->>Cache: GetTouchableViewsInPathToRoot(initialComponentView)
Cache-->>CEH: path (likely cache miss — different tag)
CEH->>CEH: onClick / onAuxClick if target in path
Reviews (1): Last reviewed commit: "Cache event path to root to avoid repeat..." | Re-trigger Greptile |
| if (componentView) { | ||
| auto tag = componentView.Tag(); | ||
| if (tag == m_cachedEventPathTag) { | ||
| return m_cachedEventPath; | ||
| } |
There was a problem hiding this comment.
Cache has no invalidation on tree mutations
The cache is keyed solely on componentView.Tag(). In Fabric, a parent view can be inserted, removed, or replaced between pointer events (via a state-driven re-render) while the leaf component retains its tag. When that happens the cached path m_cachedEventPath becomes stale, and HandleIncomingPointerEvent / IsPointerWithinInitialTree will traverse the wrong set of ancestors — firing enter/leave events on views that are no longer in the path, or missing views that were added.
A minimal fix is to clear the cache at the start of each top-level event dispatch (e.g., in onPointerMoved, onPointerPressed, etc.) rather than relying on tag identity alone.
| winrt::Microsoft::UI::Input::InputCursor m_inputCursor{nullptr}; | ||
|
|
||
| // Single-entry cache for GetTouchableViewsInPathToRoot to avoid repeated tree walks | ||
| facebook::react::Tag m_cachedEventPathTag{-1}; |
There was a problem hiding this comment.
Cache sentinel
-1 collides with valid tag values
m_cachedEventPathTag is initialised to -1. If any component view legitimately returns Tag() == -1 (a value that is also used as a sentinel elsewhere in this file, e.g. m_pointerCapturingComponentTag{-1}), the very first call to GetTouchableViewsInPathToRoot with that view would hit the cache and return an empty vector instead of computing the real path.
Using std::optional<facebook::react::Tag> for the key makes the uninitialised state unambiguous:
// .h
std::optional<facebook::react::Tag> m_cachedEventPathTag;
std::vector<winrt::Microsoft::ReactNative::ComponentView> m_cachedEventPath;// .cpp — cache-read guard
if (componentView) {
auto tag = componentView.Tag();
if (m_cachedEventPathTag.has_value() && tag == *m_cachedEventPathTag) {
return m_cachedEventPath;
}
}
// ...
if (componentView) {
m_cachedEventPathTag = componentView.Tag();
m_cachedEventPath = results;
}|
|
||
| if (m_pointerCapturingComponentTag) { |
There was a problem hiding this comment.
Unnecessary copy to
unordered_set in onPointerCaptureLost
The comment says "copy array to avoid iterator being invalidated during deletion", which was the original intent. Now that m_capturedPointers is already an unordered_set, copying it into another unordered_set is still correct but wasteful. A plain std::vector<PointerId> snapshot is lighter — it just needs to be iterable, not to support O(1) lookup:
| if (m_pointerCapturingComponentTag) { | |
| std::vector<PointerId> capturedPointers(m_capturedPointers.begin(), m_capturedPointers.end()); |
No description provided.