From 5e27acbafc46ce8d299fbe70d7926cac72caed04 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Thu, 9 Jan 2025 21:16:42 +0100 Subject: [PATCH] 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 --- libs/cdk/internals/scheduler/src/index.ts | 5 ++ .../scheduler/src/lib/scheduler.config.ts | 88 +++++++++++++++++++ .../internals/scheduler/src/lib/scheduler.ts | 26 +++--- libs/cdk/render-strategies/src/index.ts | 5 ++ .../src/lib/concurrent-strategies.ts | 18 ++-- 5 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 libs/cdk/internals/scheduler/src/lib/scheduler.config.ts diff --git a/libs/cdk/internals/scheduler/src/index.ts b/libs/cdk/internals/scheduler/src/index.ts index ac09375e9b..8aa6ddc889 100644 --- a/libs/cdk/internals/scheduler/src/index.ts +++ b/libs/cdk/internals/scheduler/src/index.ts @@ -3,4 +3,9 @@ export { forceFrameRate, scheduleCallback, } from './lib/scheduler'; +export { + provideConcurrentSchedulerConfig as ɵprovideConcurrentSchedulerConfig, + withFramerate as ɵwithFramerate, + withInputPending as ɵwithInputPending, +} from './lib/scheduler.config'; export * from './lib/schedulerPriorities'; diff --git a/libs/cdk/internals/scheduler/src/lib/scheduler.config.ts b/libs/cdk/internals/scheduler/src/lib/scheduler.config.ts new file mode 100644 index 0000000000..a0a68a813d --- /dev/null +++ b/libs/cdk/internals/scheduler/src/lib/scheduler.config.ts @@ -0,0 +1,88 @@ +import { InjectionToken, Provider } from '@angular/core'; +import { forceFrameRate, setInputPending } from './scheduler'; + +export type RX_ANGULAR_SCHEDULER_CONFIGS = 'Framerate' | 'InputPending'; + +interface RxSchedulerConfigFn { + kind: RX_ANGULAR_SCHEDULER_CONFIGS; + providers: Provider[]; +} + +const RX_SCHEDULER_DEFAULT_FPS = 60; + +// set default to 60fps +forceFrameRate(RX_SCHEDULER_DEFAULT_FPS); + +export const RX_SCHEDULER_FPS = new InjectionToken( + 'RX_SCHEDULER_FRAMERATE', + { + providedIn: 'root', + factory: () => RX_SCHEDULER_DEFAULT_FPS, + }, +); + +/** + * Provider function to specify a scheduler for `RxState` to perform state updates & emit new values. + * @param fps + */ +export function withFramerate(fps: number): unknown { + return { + kind: 'Framerate', + providers: [ + { + provide: RX_SCHEDULER_FPS, + useFactory: () => { + forceFrameRate(fps); + return fps; + }, + }, + ], + }; +} + +interface RxSchedulerInputPendingConfig { + enabled: boolean; + includeContinuous: boolean; +} + +export const RX_SCHEDULER_INPUT_PENDING = + new InjectionToken( + 'RX_SCHEDULER_INPUT_PENDING', + ); + +/** + + */ +export function withInputPending( + config: RxSchedulerInputPendingConfig = { + enabled: false, + includeContinuous: false, + }, +): RxSchedulerConfigFn { + return { + kind: 'InputPending', + providers: [ + { + provide: RX_SCHEDULER_INPUT_PENDING, + useFactory: () => { + // initialization logic here + setInputPending(config.enabled, config.includeContinuous); + return config; + }, + }, + ], + }; +} + +/** + * + */ +export function provideConcurrentSchedulerConfig( + ...configs: RxSchedulerConfigFn[] +): Provider[] { + return flatten(configs.map((c) => c.providers)); +} + +function flatten(arr: T[][]): T[] { + return arr.reduce((acc, val) => acc.concat(val), []); +} diff --git a/libs/cdk/internals/scheduler/src/lib/scheduler.ts b/libs/cdk/internals/scheduler/src/lib/scheduler.ts index 401f6c5f95..78f4136f57 100644 --- a/libs/cdk/internals/scheduler/src/lib/scheduler.ts +++ b/libs/cdk/internals/scheduler/src/lib/scheduler.ts @@ -20,7 +20,7 @@ let getCurrentTime: () => number; const hasPerformanceNow = typeof ɵglobal.performance === 'object' && typeof ɵglobal.performance.now === 'function'; - +let isInputPending = () => false; if (hasPerformanceNow) { const localPerformance = ɵglobal.performance; getCurrentTime = () => localPerformance.now(); @@ -162,12 +162,14 @@ function workLoop( currentTime = getCurrentTime(); if (typeof continuationCallback === 'function') { currentTask.callback = continuationCallback; + advanceTimers(currentTime); + return true; } else { if (currentTask === peek(taskQueue)) { pop(taskQueue); } + advanceTimers(currentTime); } - advanceTimers(currentTime); } else { pop(taskQueue); } @@ -308,25 +310,27 @@ let needsPaint = false; let queueStartTime = -1; function shouldYieldToHost() { - if (needsPaint) { + if (needsPaint || isInputPending()) { // There's a pending paint (signaled by `requestPaint`). Yield now. return true; } const timeElapsed = getCurrentTime() - queueStartTime; - if (timeElapsed < yieldInterval) { - // The main thread has only been blocked for a really short amount of time; - // smaller than a single frame. Don't yield yet. - return false; - } - - // `isInputPending` isn't available. Yield now. - return true; + return timeElapsed >= yieldInterval; } function requestPaint() { needsPaint = true; } +export function setInputPending(enable: boolean, includeContinuous = false) { + if (enable && ɵglobal.navigator?.scheduling?.isInputPending) { + isInputPending = () => + ɵglobal.navigator.scheduling.isInputPending({ includeContinuous }); + } else { + isInputPending = () => false; + } +} + export function forceFrameRate(fps) { if (fps < 0 || fps > 125) { if (typeof ngDevMode === 'undefined' || ngDevMode) { diff --git a/libs/cdk/render-strategies/src/index.ts b/libs/cdk/render-strategies/src/index.ts index bbd24cc264..253f6e4375 100644 --- a/libs/cdk/render-strategies/src/index.ts +++ b/libs/cdk/render-strategies/src/index.ts @@ -23,3 +23,8 @@ export { export { onStrategy } from './lib/onStrategy'; export { strategyHandling } from './lib/strategy-handling'; export { RxStrategyProvider } from './lib/strategy-provider.service'; +export { + ɵprovideConcurrentSchedulerConfig as provideConcurrentSchedulerConfig, + ɵwithInputPending as withExperimentalInputPending, + ɵwithFramerate as withFramerate, +} from '@rx-angular/cdk/internals/scheduler'; diff --git a/libs/cdk/render-strategies/src/lib/concurrent-strategies.ts b/libs/cdk/render-strategies/src/lib/concurrent-strategies.ts index d624c9e498..389ed72c16 100644 --- a/libs/cdk/render-strategies/src/lib/concurrent-strategies.ts +++ b/libs/cdk/render-strategies/src/lib/concurrent-strategies.ts @@ -27,7 +27,7 @@ const immediateStrategy: RxStrategyCredentials = { ngZone, priority: PriorityLevel.ImmediatePriority, scope, - }) + }), ); }, }; @@ -42,7 +42,7 @@ const userBlockingStrategy: RxStrategyCredentials = { ngZone, priority: PriorityLevel.UserBlockingPriority, scope, - }) + }), ); }, }; @@ -57,7 +57,7 @@ const normalStrategy: RxStrategyCredentials = { ngZone, priority: PriorityLevel.NormalPriority, scope, - }) + }), ); }, }; @@ -72,7 +72,7 @@ const lowStrategy: RxStrategyCredentials = { ngZone, priority: PriorityLevel.LowPriority, scope, - }) + }), ); }, }; @@ -87,7 +87,7 @@ const idleStrategy: RxStrategyCredentials = { ngZone, priority: PriorityLevel.IdlePriority, scope, - }) + }), ); }, }; @@ -99,7 +99,7 @@ function scheduleOnQueue( scope: coalescingObj; delay?: number; ngZone: NgZone; - } + }, ): MonoTypeOperatorFunction { const scope = (options.scope as Record) || {}; return (o$: Observable): Observable => @@ -115,14 +115,14 @@ function scheduleOnQueue( coalescingManager.remove(scope); subscriber.next(v); }, - { delay: options.delay, ngZone: options.ngZone } + { delay: options.delay, ngZone: options.ngZone }, ); return () => { coalescingManager.remove(scope); cancelCallback(task); }; - }).pipe(mapTo(v)) - ) + }).pipe(mapTo(v)), + ), ); }