Skip to content

Commit c93aa60

Browse files
authored
fix(schedulers): improve performance of animationFrameScheduler and asapScheduler (#7059)
* refactor(schedulers): Appropriately type action ids * fix(animationFrameScheduler): improve performance of animationFrameScheduler + Changes the check for existing action ids to simply check the last action in the queue to see if its id matches. Previously we were doing an O(n) loop on each execution of an action to check to see if the scheduling id needed to be recycled. This is problematic in AsapScheduler and AnimationFrameScheduler, where we're not reusing an interval. Since AsapScheduler and AnimationFrameScheduler reuse the most recent action id until their scheduled microtask or animation frame fires, the last action in the actions queue array is all we really need to check (rather than checking them all with `some`). O(1) vs O(n). + Refactors a weird conditional gaff from `if ((X && A) || (!X && B))` to just be `if (X ? A : B)` resolves #7017 related #7018 related #6674 * chore: update api_guardian * refactor(QueueAction): Have requestActionId return 0 Changes this to return `0` as a compromise given it was returning `void` in the past.
1 parent 2d57b38 commit c93aa60

File tree

7 files changed

+44
-28
lines changed

7 files changed

+44
-28
lines changed

api_guard/dist/types/index.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -858,8 +858,8 @@ export declare class VirtualAction<T> extends AsyncAction<T> {
858858
protected work: (this: SchedulerAction<T>, state?: T) => void;
859859
constructor(scheduler: VirtualTimeScheduler, work: (this: SchedulerAction<T>, state?: T) => void, index?: number);
860860
protected _execute(state: T, delay: number): any;
861-
protected recycleAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay?: number): any;
862-
protected requestAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay?: number): any;
861+
protected recycleAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay?: number): TimerHandle | undefined;
862+
protected requestAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay?: number): TimerHandle;
863863
schedule(state?: T, delay?: number): Subscription;
864864
}
865865

src/internal/scheduler/AnimationFrameAction.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { AsyncAction } from './AsyncAction';
22
import { AnimationFrameScheduler } from './AnimationFrameScheduler';
33
import { SchedulerAction } from '../types';
44
import { animationFrameProvider } from './animationFrameProvider';
5+
import { TimerHandle } from './timerHandle';
56

67
export class AnimationFrameAction<T> extends AsyncAction<T> {
78
constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction<T>, state?: T) => void) {
89
super(scheduler, work);
910
}
1011

11-
protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: any, delay: number = 0): any {
12+
protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {
1213
// If delay is greater than 0, request as an async action.
1314
if (delay !== null && delay > 0) {
1415
return super.requestAsyncId(scheduler, id, delay);
@@ -20,18 +21,20 @@ export class AnimationFrameAction<T> extends AsyncAction<T> {
2021
// the current animation frame request id.
2122
return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));
2223
}
23-
protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: any, delay: number = 0): any {
24+
25+
protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {
2426
// If delay exists and is greater than 0, or if the delay is null (the
2527
// action wasn't rescheduled) but was originally scheduled as an async
2628
// action, then recycle as an async action.
27-
if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {
29+
if (delay != null ? delay > 0 : this.delay > 0) {
2830
return super.recycleAsyncId(scheduler, id, delay);
2931
}
3032
// If the scheduler queue has no remaining actions with the same async id,
3133
// cancel the requested animation frame and set the scheduled flag to
3234
// undefined so the next AnimationFrameAction will request its own.
33-
if (!scheduler.actions.some((action) => action.id === id)) {
34-
animationFrameProvider.cancelAnimationFrame(id);
35+
const { actions } = scheduler;
36+
if (id != null && actions[actions.length - 1]?.id !== id) {
37+
animationFrameProvider.cancelAnimationFrame(id as number);
3538
scheduler._scheduled = undefined;
3639
}
3740
// Return undefined so the action knows to request a new async id if it's rescheduled.

src/internal/scheduler/AsapAction.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { AsyncAction } from './AsyncAction';
22
import { AsapScheduler } from './AsapScheduler';
33
import { SchedulerAction } from '../types';
44
import { immediateProvider } from './immediateProvider';
5+
import { TimerHandle } from './timerHandle';
56

67
export class AsapAction<T> extends AsyncAction<T> {
78
constructor(protected scheduler: AsapScheduler, protected work: (this: SchedulerAction<T>, state?: T) => void) {
89
super(scheduler, work);
910
}
1011

11-
protected requestAsyncId(scheduler: AsapScheduler, id?: any, delay: number = 0): any {
12+
protected requestAsyncId(scheduler: AsapScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {
1213
// If delay is greater than 0, request as an async action.
1314
if (delay !== null && delay > 0) {
1415
return super.requestAsyncId(scheduler, id, delay);
@@ -20,17 +21,19 @@ export class AsapAction<T> extends AsyncAction<T> {
2021
// the current scheduled microtask id.
2122
return scheduler._scheduled || (scheduler._scheduled = immediateProvider.setImmediate(scheduler.flush.bind(scheduler, undefined)));
2223
}
23-
protected recycleAsyncId(scheduler: AsapScheduler, id?: any, delay: number = 0): any {
24+
25+
protected recycleAsyncId(scheduler: AsapScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {
2426
// If delay exists and is greater than 0, or if the delay is null (the
2527
// action wasn't rescheduled) but was originally scheduled as an async
2628
// action, then recycle as an async action.
27-
if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {
29+
if (delay != null ? delay > 0 : this.delay > 0) {
2830
return super.recycleAsyncId(scheduler, id, delay);
2931
}
3032
// If the scheduler queue has no remaining actions with the same async id,
3133
// cancel the requested microtask and set the scheduled flag to undefined
3234
// so the next AsapAction will request its own.
33-
if (!scheduler.actions.some((action) => action.id === id)) {
35+
const { actions } = scheduler;
36+
if (id != null && actions[actions.length - 1]?.id !== id) {
3437
immediateProvider.clearImmediate(id);
3538
scheduler._scheduled = undefined;
3639
}

src/internal/scheduler/AsyncAction.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { Subscription } from '../Subscription';
44
import { AsyncScheduler } from './AsyncScheduler';
55
import { intervalProvider } from './intervalProvider';
66
import { arrRemove } from '../util/arrRemove';
7+
import { TimerHandle } from './timerHandle';
78

89
export class AsyncAction<T> extends Action<T> {
9-
public id: any;
10+
public id: TimerHandle | undefined;
1011
public state?: T;
1112
// @ts-ignore: Property has no initializer and is not definitely assigned
1213
public delay: number;
@@ -58,23 +59,26 @@ export class AsyncAction<T> extends Action<T> {
5859

5960
this.delay = delay;
6061
// If this action has already an async Id, don't request a new one.
61-
this.id = this.id || this.requestAsyncId(scheduler, this.id, delay);
62+
this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);
6263

6364
return this;
6465
}
6566

66-
protected requestAsyncId(scheduler: AsyncScheduler, _id?: any, delay: number = 0): any {
67+
protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {
6768
return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);
6869
}
6970

70-
protected recycleAsyncId(_scheduler: AsyncScheduler, id: any, delay: number | null = 0): any {
71+
protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {
7172
// If this action is rescheduled with the same delay time, don't clear the interval id.
7273
if (delay != null && this.delay === delay && this.pending === false) {
7374
return id;
7475
}
7576
// Otherwise, if the action's delay time is different from the current delay,
7677
// or the action has been rescheduled before it's executed, clear the interval id
77-
intervalProvider.clearInterval(id);
78+
if (id != null) {
79+
intervalProvider.clearInterval(id);
80+
}
81+
7882
return undefined;
7983
}
8084

src/internal/scheduler/AsyncScheduler.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Scheduler } from '../Scheduler';
22
import { Action } from './Action';
33
import { AsyncAction } from './AsyncAction';
4+
import { TimerHandle } from './timerHandle';
45

56
export class AsyncScheduler extends Scheduler {
67
public actions: Array<AsyncAction<any>> = [];
@@ -18,7 +19,7 @@ export class AsyncScheduler extends Scheduler {
1819
* @type {any}
1920
* @internal
2021
*/
21-
public _scheduled: any = undefined;
22+
public _scheduled: TimerHandle | undefined;
2223

2324
constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {
2425
super(SchedulerAction, now);

src/internal/scheduler/QueueAction.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import { AsyncAction } from './AsyncAction';
22
import { Subscription } from '../Subscription';
33
import { QueueScheduler } from './QueueScheduler';
44
import { SchedulerAction } from '../types';
5+
import { TimerHandle } from './timerHandle';
56

67
export class QueueAction<T> extends AsyncAction<T> {
7-
8-
constructor(protected scheduler: QueueScheduler,
9-
protected work: (this: SchedulerAction<T>, state?: T) => void) {
8+
constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction<T>, state?: T) => void) {
109
super(scheduler, work);
1110
}
1211

@@ -21,20 +20,25 @@ export class QueueAction<T> extends AsyncAction<T> {
2120
}
2221

2322
public execute(state: T, delay: number): any {
24-
return (delay > 0 || this.closed) ?
25-
super.execute(state, delay) :
26-
this._execute(state, delay) ;
23+
return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);
2724
}
2825

29-
protected requestAsyncId(scheduler: QueueScheduler, id?: any, delay: number = 0): any {
26+
protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {
3027
// If delay exists and is greater than 0, or if the delay is null (the
3128
// action wasn't rescheduled) but was originally scheduled as an async
3229
// action, then recycle as an async action.
3330

3431
if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {
3532
return super.requestAsyncId(scheduler, id, delay);
3633
}
34+
3735
// Otherwise flush the scheduler starting with this action.
38-
return scheduler.flush(this);
36+
scheduler.flush(this);
37+
38+
// HACK: In the past, this was returning `void`. However, `void` isn't a valid
39+
// `TimerHandle`, and generally the return value here isn't really used. So the
40+
// compromise is to return `0` which is both "falsy" and a valid `TimerHandle`,
41+
// as opposed to refactoring every other instanceo of `requestAsyncId`.
42+
return 0;
3943
}
4044
}

src/internal/scheduler/VirtualTimeScheduler.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AsyncAction } from './AsyncAction';
22
import { Subscription } from '../Subscription';
33
import { AsyncScheduler } from './AsyncScheduler';
44
import { SchedulerAction } from '../types';
5+
import { TimerHandle } from './timerHandle';
56

67
export class VirtualTimeScheduler extends AsyncScheduler {
78
/** @deprecated Not used in VirtualTimeScheduler directly. Will be removed in v8. */
@@ -92,15 +93,15 @@ export class VirtualAction<T> extends AsyncAction<T> {
9293
}
9394
}
9495

95-
protected requestAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay: number = 0): any {
96+
protected requestAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay: number = 0): TimerHandle {
9697
this.delay = scheduler.frame + delay;
9798
const { actions } = scheduler;
9899
actions.push(this);
99100
(actions as Array<VirtualAction<T>>).sort(VirtualAction.sortActions);
100-
return true;
101+
return 1;
101102
}
102103

103-
protected recycleAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay: number = 0): any {
104+
protected recycleAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay: number = 0): TimerHandle | undefined {
104105
return undefined;
105106
}
106107

0 commit comments

Comments
 (0)