Skip to content

Commit 5e27acb

Browse files
committed
feat(cdk): make scheduler features configurable
This commit introduces the provideConcurrentSchedulerConfig provider function. Developers are now able to configure the scheduler via the angular DI system. We've also added the `isInputPending` feature. It allows to stop the execution of the work queue while a user is doing something on the page
1 parent d12d2db commit 5e27acb

File tree

5 files changed

+122
-20
lines changed

5 files changed

+122
-20
lines changed

libs/cdk/internals/scheduler/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ export {
33
forceFrameRate,
44
scheduleCallback,
55
} from './lib/scheduler';
6+
export {
7+
provideConcurrentSchedulerConfig as ɵprovideConcurrentSchedulerConfig,
8+
withFramerate as ɵwithFramerate,
9+
withInputPending as ɵwithInputPending,
10+
} from './lib/scheduler.config';
611
export * from './lib/schedulerPriorities';
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { InjectionToken, Provider } from '@angular/core';
2+
import { forceFrameRate, setInputPending } from './scheduler';
3+
4+
export type RX_ANGULAR_SCHEDULER_CONFIGS = 'Framerate' | 'InputPending';
5+
6+
interface RxSchedulerConfigFn {
7+
kind: RX_ANGULAR_SCHEDULER_CONFIGS;
8+
providers: Provider[];
9+
}
10+
11+
const RX_SCHEDULER_DEFAULT_FPS = 60;
12+
13+
// set default to 60fps
14+
forceFrameRate(RX_SCHEDULER_DEFAULT_FPS);
15+
16+
export const RX_SCHEDULER_FPS = new InjectionToken<number>(
17+
'RX_SCHEDULER_FRAMERATE',
18+
{
19+
providedIn: 'root',
20+
factory: () => RX_SCHEDULER_DEFAULT_FPS,
21+
},
22+
);
23+
24+
/**
25+
* Provider function to specify a scheduler for `RxState` to perform state updates & emit new values.
26+
* @param fps
27+
*/
28+
export function withFramerate(fps: number): unknown {
29+
return {
30+
kind: 'Framerate',
31+
providers: [
32+
{
33+
provide: RX_SCHEDULER_FPS,
34+
useFactory: () => {
35+
forceFrameRate(fps);
36+
return fps;
37+
},
38+
},
39+
],
40+
};
41+
}
42+
43+
interface RxSchedulerInputPendingConfig {
44+
enabled: boolean;
45+
includeContinuous: boolean;
46+
}
47+
48+
export const RX_SCHEDULER_INPUT_PENDING =
49+
new InjectionToken<RxSchedulerInputPendingConfig>(
50+
'RX_SCHEDULER_INPUT_PENDING',
51+
);
52+
53+
/**
54+
55+
*/
56+
export function withInputPending(
57+
config: RxSchedulerInputPendingConfig = {
58+
enabled: false,
59+
includeContinuous: false,
60+
},
61+
): RxSchedulerConfigFn {
62+
return {
63+
kind: 'InputPending',
64+
providers: [
65+
{
66+
provide: RX_SCHEDULER_INPUT_PENDING,
67+
useFactory: () => {
68+
// initialization logic here
69+
setInputPending(config.enabled, config.includeContinuous);
70+
return config;
71+
},
72+
},
73+
],
74+
};
75+
}
76+
77+
/**
78+
*
79+
*/
80+
export function provideConcurrentSchedulerConfig(
81+
...configs: RxSchedulerConfigFn[]
82+
): Provider[] {
83+
return flatten(configs.map((c) => c.providers));
84+
}
85+
86+
function flatten<T>(arr: T[][]): T[] {
87+
return arr.reduce((acc, val) => acc.concat(val), []);
88+
}

libs/cdk/internals/scheduler/src/lib/scheduler.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ let getCurrentTime: () => number;
2020
const hasPerformanceNow =
2121
typeof ɵglobal.performance === 'object' &&
2222
typeof ɵglobal.performance.now === 'function';
23-
23+
let isInputPending = () => false;
2424
if (hasPerformanceNow) {
2525
const localPerformance = ɵglobal.performance;
2626
getCurrentTime = () => localPerformance.now();
@@ -162,12 +162,14 @@ function workLoop(
162162
currentTime = getCurrentTime();
163163
if (typeof continuationCallback === 'function') {
164164
currentTask.callback = continuationCallback;
165+
advanceTimers(currentTime);
166+
return true;
165167
} else {
166168
if (currentTask === peek(taskQueue)) {
167169
pop(taskQueue);
168170
}
171+
advanceTimers(currentTime);
169172
}
170-
advanceTimers(currentTime);
171173
} else {
172174
pop(taskQueue);
173175
}
@@ -308,25 +310,27 @@ let needsPaint = false;
308310
let queueStartTime = -1;
309311

310312
function shouldYieldToHost() {
311-
if (needsPaint) {
313+
if (needsPaint || isInputPending()) {
312314
// There's a pending paint (signaled by `requestPaint`). Yield now.
313315
return true;
314316
}
315317
const timeElapsed = getCurrentTime() - queueStartTime;
316-
if (timeElapsed < yieldInterval) {
317-
// The main thread has only been blocked for a really short amount of time;
318-
// smaller than a single frame. Don't yield yet.
319-
return false;
320-
}
321-
322-
// `isInputPending` isn't available. Yield now.
323-
return true;
318+
return timeElapsed >= yieldInterval;
324319
}
325320

326321
function requestPaint() {
327322
needsPaint = true;
328323
}
329324

325+
export function setInputPending(enable: boolean, includeContinuous = false) {
326+
if (enable && ɵglobal.navigator?.scheduling?.isInputPending) {
327+
isInputPending = () =>
328+
ɵglobal.navigator.scheduling.isInputPending({ includeContinuous });
329+
} else {
330+
isInputPending = () => false;
331+
}
332+
}
333+
330334
export function forceFrameRate(fps) {
331335
if (fps < 0 || fps > 125) {
332336
if (typeof ngDevMode === 'undefined' || ngDevMode) {

libs/cdk/render-strategies/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ export {
2323
export { onStrategy } from './lib/onStrategy';
2424
export { strategyHandling } from './lib/strategy-handling';
2525
export { RxStrategyProvider } from './lib/strategy-provider.service';
26+
export {
27+
ɵprovideConcurrentSchedulerConfig as provideConcurrentSchedulerConfig,
28+
ɵwithInputPending as withExperimentalInputPending,
29+
ɵwithFramerate as withFramerate,
30+
} from '@rx-angular/cdk/internals/scheduler';

libs/cdk/render-strategies/src/lib/concurrent-strategies.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const immediateStrategy: RxStrategyCredentials = {
2727
ngZone,
2828
priority: PriorityLevel.ImmediatePriority,
2929
scope,
30-
})
30+
}),
3131
);
3232
},
3333
};
@@ -42,7 +42,7 @@ const userBlockingStrategy: RxStrategyCredentials = {
4242
ngZone,
4343
priority: PriorityLevel.UserBlockingPriority,
4444
scope,
45-
})
45+
}),
4646
);
4747
},
4848
};
@@ -57,7 +57,7 @@ const normalStrategy: RxStrategyCredentials = {
5757
ngZone,
5858
priority: PriorityLevel.NormalPriority,
5959
scope,
60-
})
60+
}),
6161
);
6262
},
6363
};
@@ -72,7 +72,7 @@ const lowStrategy: RxStrategyCredentials = {
7272
ngZone,
7373
priority: PriorityLevel.LowPriority,
7474
scope,
75-
})
75+
}),
7676
);
7777
},
7878
};
@@ -87,7 +87,7 @@ const idleStrategy: RxStrategyCredentials = {
8787
ngZone,
8888
priority: PriorityLevel.IdlePriority,
8989
scope,
90-
})
90+
}),
9191
);
9292
},
9393
};
@@ -99,7 +99,7 @@ function scheduleOnQueue<T>(
9999
scope: coalescingObj;
100100
delay?: number;
101101
ngZone: NgZone;
102-
}
102+
},
103103
): MonoTypeOperatorFunction<T> {
104104
const scope = (options.scope as Record<string, unknown>) || {};
105105
return (o$: Observable<T>): Observable<T> =>
@@ -115,14 +115,14 @@ function scheduleOnQueue<T>(
115115
coalescingManager.remove(scope);
116116
subscriber.next(v);
117117
},
118-
{ delay: options.delay, ngZone: options.ngZone }
118+
{ delay: options.delay, ngZone: options.ngZone },
119119
);
120120
return () => {
121121
coalescingManager.remove(scope);
122122
cancelCallback(task);
123123
};
124-
}).pipe(mapTo(v))
125-
)
124+
}).pipe(mapTo(v)),
125+
),
126126
);
127127
}
128128

0 commit comments

Comments
 (0)