Skip to content

Commit

Permalink
Optimize data structure to store listeners in EventTarget (facebook#4…
Browse files Browse the repository at this point in the history
…8964)

Summary:

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    |

Differential Revision: D68671944
  • Loading branch information
rubennorte authored and facebook-github-bot committed Jan 30, 2025
1 parent 69b06bd commit 20716c5
Showing 1 changed file with 34 additions and 50 deletions.
84 changes: 34 additions & 50 deletions packages/react-native/src/private/webapis/dom/events/EventTarget.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type EventListenerRegistration = {
removed: boolean,
};

type ListenersMap = Map<string, Array<EventListenerRegistration>>;
type ListenersMap = Map<string, Map<EventListener, EventListenerRegistration>>;

export default class EventTarget {
addEventListener(
Expand Down Expand Up @@ -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 = {
Expand All @@ -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,
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -396,25 +395,10 @@ function invoke(
}
}

function removeEventListenerRegistration(
registration: EventListenerRegistration,
listenerList: Array<EventListenerRegistration>,
): 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 {
Expand Down

0 comments on commit 20716c5

Please sign in to comment.