10000 fix: lazy-clone listeners array · NativeScript/NativeScript@95f9c44 · GitHub
[go: up one dir, main page]

Skip to content

Commit 95f9c44

Browse files
committed
fix: lazy-clone listeners array
1 parent f58d743 commit 95f9c44

File tree

3 files changed

+116
-18
lines changed

3 files changed

+116
-18
lines changed

packages/core/data/dom-events/dom-event.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { EventData, ListenerEntry, Observable } from '../observable/index';
22
import type { ViewBase } from '../../ui/core/view-base';
3+
import { MutationSensitiveArray } from '../mutation-sensitive-array';
34

45
// This file contains some of Core's hot paths, so attention has been taken to
56
// optimise it. Where specified, optimisations made have been informed based on
@@ -13,7 +14,7 @@ const timeOrigin = Date.now();
1314
* optional accesses, so reusing the same one and treating it as immutable
1415
* avoids unnecessary allocations on a relatively hot path of the library.
1516
*/
16-
const emptyArray = [] as const;
17+
const emptyArray = new MutationSensitiveArray<ListenerEntry>();
1718

1819
export class DOMEvent implements Event {
1920
/**
@@ -223,7 +224,7 @@ export class DOMEvent implements Event {
223224
* event's cancelable attribute value is false or its preventDefault()
224225
* method was not invoked, and false otherwise.
225226
*/
226-
dispatchTo({ target, data, getGlobalEventHandlersPreHandling, getGlobalEventHandlersPostHandling }: { target: Observable; data: EventData; getGlobalEventHandlersPreHandling?: () => readonly ListenerEntry[]; getGlobalEventHandlersPostHandling?: () => readonly ListenerEntry[] }): boolean {
227+
dispatchTo({ target, data, getGlobalEventHandlersPreHandling, getGlobalEventHandlersPostHandling }: { target: Observable; data: EventData; getGlobalEventHandlersPreHandling?: () => MutationSensitiveArray<ListenerEntry>; getGlobalEventHandlersPostHandling?: () => MutationSensitiveArray<ListenerEntry> }): boolean {
227228
if (this.eventPhase !== this.NONE) {
228229
throw new Error('Tried to dispatch a dispatching event');
229230
}
@@ -340,16 +341,31 @@ export class DOMEvent implements Event {
340341
return this.returnValue;
341342
}
342343

343-
private handleEvent({ data, isGlobal, getListenersForType, phase, removeEventListener }: { data: EventData; isGlobal: boolean; getListenersForType: () => readonly ListenerEntry[]; phase: 0 | 1 | 2 | 3; removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void }) {
344-
// Work on a copy of the array, as any callback could modify the
345-
// original array during the loop.
344+
private handleEvent({ data, isGlobal, getListenersForType, phase, removeEventListener }: { data: EventData; isGlobal: boolean; getListenersForType: () => MutationSensitiveArray<ListenerEntry>; phase: 0 | 1 | 2 | 3; removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void }) {
345+
// We want to work on a copy of the array, as any callback could modify
346+
// the original array during the loop.
346347
//
347-
// Cloning the array via spread syntax is up to 180 nanoseconds faster
348-
// per run than using Array.prototype.slice().
349-
const listenersForTypeCopy = [...getListenersForType()];
348+
// However, cloning arrays is expensive on this hot path, so we'll do it
349+
// lazily - i.e. only take a clone if a mutation is about to happen.
350+
// This optimisation is particularly worth doing as it's very rare that
351+
// an event listener callback will end up modifying the listeners array.
352+
const listenersLive: MutationSensitiveArray<ListenerEntry> = getListenersForType();
353+
354+
// Set a listener to clone the array just before any mutations.
355+
let listenersLazyCopy: ListenerEntry[] = listenersLive;
356+
const doLazyCopy = () => {
357+
// Cloning the array via spread syntax is up to 180 nanoseconds
358+
// faster per run than using Array.prototype.slice().
359+
listenersLazyCopy = [...listenersLive];
360+
};
361+
listenersLive.once(doLazyCopy);
362+
363+
// Make sure we remove the listener before we exit the function,
364+
// otherwise we may wastefully clone the array.
365+
const cleanup = () => listenersLive.removeListener(doLazyCopy);
350366

351-
for (let i = listenersForTypeCopy.length - 1; i >= 0; i--) {
352-
const listener = listenersForTypeCopy[i];
367+
for (let i = listenersLazyCopy.length - 1; i >= 0; i--) {
368+
const listener = listenersLazyCopy[i];
353369

354370
// Assigning variables this old-fashioned way is up to 50
355371
// nanoseconds faster per run than ESM destructuring syntax.
@@ -365,7 +381,7 @@ export class DOMEvent implements Event {
365381
// We simply use a strict equality check here because we trust that
366382
// the listeners provider will never allow two deeply-equal
367383
// listeners into the array.
368-
if (!getListenersForType().includes(listener)) {
384+
if (!listenersLive.includes(listener)) {
369385
continue;
370386
}
371387

@@ -395,9 +411,12 @@ export class DOMEvent implements Event {
395411
}
396412

397413
if (this.propagationState === EventPropagationState.stopImmediate) {
414+
cleanup();
398415
return;
399416
}
400417
}
418+
419+
cleanup();
401420
}
402421
}
403422

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* A lightweight extension of Array that calls listeners just before any
3+
* mutations. This allows you to lazily take a clone of an array (i.e. use the
4+
* array as-is until such time as it mutates).
5+
*
6+
* This could equally be implemented by adding pre-mutation events into
7+
* ObservableArray, but the whole point is to be as lightweight as possible as
8+
* its entire purpose is to be used for performance-sensitive tasks.
9+
*/
10+
export class MutationSensitiveArray<T> extends Array<T> {
11+
private readonly listeners: (() => void)[] = [];
12+
13+
once(listener: () => void): void {
14+
const wrapper = () => {
15+
listener();
16+
this.removeListener(wrapper);
17+
};
18+
this.addListener(wrapper);
19+
}
20+
21+
addListener(listener: () => void): void {
22+
if (!this.listeners.includes(listener)) {
23+
this.listeners.push(listener);
24+
}
25+
}
26+
27+
removeListener(listener: () => void): void {
28+
const index = this.listeners.indexOf(listener);
29+
if (index > -1) {
30+
this.listeners.splice(index, 1);
31+
}
32+
}
33+
34+
private invalidate(): void {
35+
for (const listener of this.listeners) {
36+
listener();
37+
}
38+
}
39+
40+
// Override each mutating Array method so that it invalidates our snapshot.
41+
42+
pop(): T | undefined {
43+
this.invalidate();
44+
return super.pop();
45+
}
46+
push(...items: T[]): number {
47+
this.invalidate();
48+
return super.push(...items);
49+
}
50+
reverse(): T[] {
51+
this.invalidate();
52+
return super.reverse();
53+
}
54+
shift(): T | undefined {
55+
this.invalidate();
56+
return super.shift();
57+
}
58+
sort(compareFn?: (a: T, b: T) => number): this {
59+
this.invalidate();
60+
return super.sort(compareFn);
61+
}
62+
splice(start: number, deleteCount: number, ...rest: T[]): T[] {
63+
this.invalidate();
64+
return super.splice(start, deleteCount, ...rest);
65+
}
66+
unshift(...items: T[]): number {
67+
this.invalidate();
68+
return super.unshift(...items);
69+
}
70+
fill(value: T, start?: number, end?: number): this {
71+
this.invalidate();
72+
return super.fill(value, start, end);
73+
}
74+
copyWithin(target: number, start: number, end?: number): this {
75+
this.invalidate();
76+
return super.copyWithin(target, start, end);
77+
}
78+
}

packages/core/data/observable/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ViewBase } from '../../ui/core/view-base';
22
import { DOMEvent } from '../dom-events/dom-event';
3+
import { MutationSensitiveArray } from '../mutation-sensitive-array';
34

45
/**
56
* Base event data.
@@ -85,7 +86,7 @@ const _wrappedValues = [new WrappedValue(null), new WrappedValue(null), new Wrap
8586

8687
const _globalEventHandlers: {
8788
[eventClass: string]: {
88-
[eventName: string]: ListenerEntry[];
89+
[eventName: string]: MutationSensitiveArray<ListenerEntry>;
8990
};
9091
} = {};
9192

@@ -114,7 +115,7 @@ export class Observable implements EventTarget {
114115
return this._isViewBase;
115116
}
116117

117-
private readonly _observers: { [eventName: string]: ListenerEntry[] } = {};
118+
private readonly _observers: { [eventName: string]: MutationSensitiveArray<ListenerEntry> } = {};
118119

119120
public get(name: string): any {
120121
return this[name];
@@ -313,7 +314,7 @@ export class Observable implements EventTarget {
313314
_globalEventHandlers[eventClass] = {};
314315
}
315316
if (!Array.isArray(_globalEventHandlers[eventClass][eventName])) {
316-
_globalEventHandlers[eventClass][eventName] = [];
317+
_globalEventHandlers[eventClass][eventName] = new MutationSensitiveArray();
317318
}
318319

319320
const list = _globalEventHandlers[eventClass][eventName];
@@ -399,11 +400,11 @@ export class Observable implements EventTarget {
399400
});
400401
}
401402

402-
private _getGlobalEventHandlers(data: EventData, eventType: 'First' | ''): ListenerEntry[] {
403+
private _getGlobalEventHandlers(data: EventData, eventType: 'First' | ''): MutationSensitiveArray<ListenerEntry> {
403404
const eventClass = data.object?.constructor?.name;
404405
const globalEventHandlersForOwnClass = _globalEventHandlers[eventClass]?.[`${data.eventName}${eventType}`] ?? [];
405406
const globalEventHandlersForAllClasses = _globalEventHandlers['*']?.[`${data.eventName}${eventType}`] ?? [];
406-
return [...globalEventHandlersForOwnClass, ...globalEventHandlersForAllClasses];
407+
return new MutationSensitiveArray(...globalEventHandlersForOwnClass, ...globalEventHandlersForAllClasses);
407408
}
408409

409410
/**
@@ -440,14 +441,14 @@ export class Observable implements EventTarget {
440441
}
441442
}
442443

443-
public getEventList(eventName: string, createIfNeeded?: boolean): ListenerEntry[] | undefined {
444+
public getEventList(eventName: string, createIfNeeded?: boolean): MutationSensitiveArray<ListenerEntry> | undefined {
444445
if (!eventName) {
445446
throw new TypeError('EventName must be valid string.');
446447
}
447448

448449
let list = this._observers[eventName];
449450
if (!list && createIfNeeded) {
450-
list = [];
451+
list = new MutationSensitiveArray();
451452
this._observers[eventName] = list;
452453
}
453454

0 commit comments

Comments
 (0)
0