From 3d4906e7ec7018d656d6f798451d4e4972a51f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 31 Jan 2025 04:00:31 -0800 Subject: [PATCH] Optimize data structure to store listeners in EventTarget (#48964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48964 Changelog: [internal] This replaces the data structure used to store listeners in `EventTarget`, from a map of arrays to a map of maps. This essentially optimizes listener registration/deregistraton at the expense of event dispatching. Given that it'll be common to have many nodes registering to events that are never dispatched, this might be the right trade-off. * Before: | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | --------------------------------------------------------------------- | --------------------- | ---------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'dispatchEvent, no bubbling, no listeners' | '4624.68 ± 0.49%' | '4570.00' | '218089 ± 0.02%' | '218818' | 216232 | | 1 | 'dispatchEvent, no bubbling, single listener' | '5771.34 ± 0.99%' | '5670.00' | '175389 ± 0.02%' | '176367' | 173270 | | 2 | 'dispatchEvent, no bubbling, multiple listeners' | '48207.35 ± 1.18%' | '47290.00' | '20964 ± 0.04%' | '21146' | 20744 | | 3 | 'dispatchEvent, bubbling, no listeners' | '185005.29 ± 0.16%' | '184060.00' | '5410 ± 0.05%' | '5433' | 5406 | | 4 | 'dispatchEvent, bubbling, single listener per target' | '286630.57 ± 0.11%' | '285560.00' | '3491 ± 0.06%' | '3502' | 3489 | | 5 | 'dispatchEvent, bubbling, multiple listeners per target' | '4435944.62 ± 0.27%' | '4425840.00 ± 30.00' | '226 ± 0.12%' | '226' | 1000 | | 6 | 'addEventListener, one listener' | '1734.88 ± 0.57%' | '1670.00' | '594938 ± 0.01%' | '598802' | 576411 | | 7 | 'addEventListener, one target, one type, multiple listeners' | '266031.11 ± 0.68%' | '261810.00' | '3781 ± 0.15%' | '3820' | 3759 | | 8 | 'addEventListener, one target, multiple types, one listener per type' | '124768.56 ± 0.39%' | '121160.00' | '8112 ± 0.16%' | '8254' | 8015 | | 9 | 'addEventListener, one target, multiple types, multiple listeners' | '27141326.31 ± 0.15%' | '27298945.00 ± 115.00' | '37 ± 0.15%' | '37' | 1000 | | 10 | 'addEventListener, multiple targets, one type, one listener' | '142646.24 ± 0.49%' | '137460.00' | '7123 ± 0.19%' | '7275' | 7011 | * After: | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | --------------------------------------------------------------------- | --------------------- | --------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'dispatchEvent, no bubbling, no listeners' | '4518.27 ± 0.51%' | '4460.00' | '223269 ± 0.02%' | '224215' | 221324 | | 1 | 'dispatchEvent, no bubbling, single listener' | '6563.10 ± 0.98%' | '6450.00' | '154311 ± 0.02%' | '155039' | 152367 | | 2 | 'dispatchEvent, no bubbling, multiple listeners' | '65429.69 ± 0.25%' | '64840.00' | '15330 ± 0.05%' | '15423' | 15284 | | 3 | 'dispatchEvent, bubbling, no listeners' | '181104.21 ± 0.13%' | '180300.00' | '5525 ± 0.05%' | '5546' | 5522 | | 4 | 'dispatchEvent, bubbling, single listener per target' | '367231.05 ± 0.10%' | '366035.00 ± 5.00' | '2724 ± 0.07%' | '2732' | 2724 | | 5 | 'dispatchEvent, bubbling, multiple listeners per target' | '6269141.58 ± 0.24%' | '6253275.00 ± 155.00' | '160 ± 0.11%' | '160' | 1000 | | 6 | 'addEventListener, one listener' | '1665.23 ± 0.51%' | '1610.00' | '618122 ± 0.01%' | '621118' | 600517 | | 7 | 'addEventListener, one target, one type, multiple listeners' | '97724.12 ± 1.34%' | '94640.00' | '10433 ± 0.14%' | '10566' | 10233 | | 8 | 'addEventListener, one target, multiple types, one listener per type' | '116915.60 ± 0.54%' | '113380.00' | '8707 ± 0.17%' | '8820' | 8554 | | 9 | 'addEventListener, one target, multiple types, multiple listeners' | '12276537.38 ± 0.42%' | '11924070.00 ± 10.00' | '82 ± 0.35%' | '84' | 1000 | | 10 | 'addEventListener, multiple targets, one type, one listener' | '133307.67 ± 0.58%' | '128340.00' | '7666 ± 0.20%' | '7792' | 7502 | Reviewed By: rshest Differential Revision: D68671944 fbshipit-source-id: 1a555ba9400e5830b4a56cdf12fd9418cd4a2a4f --- .../private/webapis/dom/events/EventTarget.js | 84 ++++++++----------- 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/packages/react-native/src/private/webapis/dom/events/EventTarget.js b/packages/react-native/src/private/webapis/dom/events/EventTarget.js index 5e70029d046..61e1efb83af 100644 --- a/packages/react-native/src/private/webapis/dom/events/EventTarget.js +++ b/packages/react-native/src/private/webapis/dom/events/EventTarget.js @@ -57,7 +57,7 @@ type EventListenerRegistration = { removed: boolean, }; -type ListenersMap = Map>; +type ListenersMap = Map>; export default class EventTarget { addEventListener( @@ -112,21 +112,17 @@ export default class EventTarget { return; } - let listenersMap = getListenersMap(this, capture); - let listenerList = listenersMap?.get(processedType); - if (listenerList == null) { - if (listenersMap == null) { - listenersMap = new Map(); - setListenersMap(this, capture, listenersMap); - } - listenerList = []; - listenersMap.set(processedType, listenerList); - } else { - for (const listener of listenerList) { - if (listener.callback === callback) { - return; - } + let listenersByType = getListenersForPhase(this, capture); + let listeners = listenersByType?.get(processedType); + if (listeners == null) { + if (listenersByType == null) { + listenersByType = new Map(); + setListenersMap(this, capture, listenersByType); } + listeners = new Map(); + listenersByType.set(processedType, listeners); + } else if (listeners.has(callback)) { + return; } const listener: EventListenerRegistration = { @@ -135,15 +131,18 @@ export default class EventTarget { once, removed: false, }; - listenerList.push(listener); + listeners.set(callback, listener); - const nonNullListenerList = listenerList; + const nonNullListeners = listeners; if (signal != null) { signal.addEventListener( 'abort', () => { - removeEventListenerRegistration(listener, nonNullListenerList); + listener.removed = true; + if (nonNullListeners.get(callback) === listener) { + nonNullListeners.delete(callback); + } }, { once: true, @@ -176,20 +175,16 @@ export default class EventTarget { ? optionsOrUseCapture : Boolean(optionsOrUseCapture.capture); - const listenersMap = getListenersMap(this, capture); - const listenerList = listenersMap?.get(processedType); - if (listenerList == null) { + const listenersByType = getListenersForPhase(this, capture); + const listeners = listenersByType?.get(processedType); + if (listeners == null) { return; } - for (let i = 0; i < listenerList.length; i++) { - const listener = listenerList[i]; - - if (listener.callback === callback) { - listener.removed = true; - listenerList.splice(i, 1); - return; - } + const listener = listeners.get(callback); + if (listener != null) { + listener.removed = true; + listeners.delete(callback); } } @@ -335,22 +330,26 @@ function invoke( event: Event, eventPhase: EventPhase, ) { - const listenersMap = getListenersMap( + const listenersByType = getListenersForPhase( eventTarget, eventPhase === Event.CAPTURING_PHASE, ); setCurrentTarget(event, eventTarget); - // This is a copy so listeners added during dispatch are NOT executed. - const listenerList = listenersMap?.get(event.type)?.slice(); - if (listenerList == null) { + const maybeListeners = listenersByType?.get(event.type); + if (maybeListeners == null) { return; } + // This is a copy so listeners added during dispatch are NOT executed. + // Note that `maybeListeners.values()` is a live view of the map instead of an + // immutable copy. + const listeners = Array.from(maybeListeners.values()); + setCurrentTarget(event, eventTarget); - for (const listener of listenerList) { + for (const listener of listeners) { if (listener.removed) { continue; } @@ -396,25 +395,10 @@ function invoke( } } -function removeEventListenerRegistration( - registration: EventListenerRegistration, - listenerList: Array, -): void { - for (let i = 0; i < listenerList.length; i++) { - const listener = listenerList[i]; - - if (listener === registration) { - listener.removed = true; - listenerList.splice(i, 1); - return; - } - } -} - const CAPTURING_LISTENERS_KEY = Symbol('capturingListeners'); const BUBBLING_LISTENERS_KEY = Symbol('bubblingListeners'); -function getListenersMap( +function getListenersForPhase( eventTarget: EventTarget, isCapture: boolean, ): ?ListenersMap {