Skip to content

Instantly share code, notes, and snippets.

@schickling
Created August 18, 2023 07:08
Show Gist options
  • Select an option

  • Save schickling/a07dcb8b6bb87c61c3b3f8b56af04d12 to your computer and use it in GitHub Desktop.

Select an option

Save schickling/a07dcb8b6bb87c61c3b3f8b56af04d12 to your computer and use it in GitHub Desktop.
/* eslint-disable prefer-arrow/prefer-arrow-functions */
export * from '@effect/io/Scheduler'
import * as Scheduler from '@effect/io/Scheduler'
/** NOTE should only be used on Main Thread */
export class BackgroundScheduler implements Scheduler.Scheduler {
private running = false
private taskBuckets = new Scheduler.PriorityBuckets()
scheduleTask(task: Scheduler.Task, priority: number) {
this.taskBuckets.scheduleTask(task, priority)
if (this.running === false) {
this.running = true
this.starve()
}
}
private async doWork(idleDeadline: IdleDeadline) {
while (true) {
const task = getNextTask(this.taskBuckets)
if (task === undefined) {
this.running = false
return
} else {
task()
if (navigator.scheduling!.isInputPending!() || idleDeadline.timeRemaining() <= 0.5) {
this.starve()
return
}
}
}
}
starve() {
requestIdleCallback((idleDeadline) => {
this.doWork(idleDeadline)
})
}
}
export class FallbackBackgroundScheduler implements Scheduler.Scheduler {
private running = false
private taskBuckets = new Scheduler.PriorityBuckets()
/** Timestamp in milli seconds */
private lastWitnessedFrame = 0
private timePerFrame = 1000 / this.systemFps
constructor(private systemFps = 60) {
// NOTE We keep this loop running (even if no tasks might be scheduled for a while)
// to optimize the latency for the next task that might be scheduled
this.loopRequestAnimationFrame()
}
scheduleTask(task: Scheduler.Task, priority: number) {
this.taskBuckets.scheduleTask(task, priority)
if (this.running === false) {
this.running = true
this.starve()
}
}
// TODO We should investigate a better implementation for Safari if possible
private loopRequestAnimationFrame() {
// TODO use `_now` from requestAnimationFrame
requestAnimationFrame((_now) => {
// setInterval(() => {
this.lastWitnessedFrame = Date.now()
this.loopRequestAnimationFrame()
})
// }, this.timePerFrame)
}
private starve() {
while (true) {
const task = getNextTask(this.taskBuckets)
if (task === undefined) {
this.running = false
return
} else {
task()
if (this.getRemainingTimeMs() <= 0.5) {
this.starve()
return
}
}
}
}
private getRemainingTimeMs() {
const now = Date.now()
const timeSinceLastFrame = now - this.lastWitnessedFrame
return this.timePerFrame - timeSinceLastFrame
}
}
/**
* NOTE This implementation always prioritizes higher-priority tasks over lower priority tasks
* even if the lower priority task was scheduled first
*/
const getNextTask = (taskBuckets: Scheduler.PriorityBuckets): Scheduler.Task | undefined => {
for (const [_, tasks] of taskBuckets.buckets) {
const task = tasks.shift()
if (task !== undefined) return task
}
}
export const backgroundScheduler = (): Scheduler.Scheduler => {
// Safari doesn't yet support `requestIdleCallback` and `navigator.scheduling.isInputPending`
// So we're falling back to a timer based scheduler
if (
typeof window === 'undefined' ||
window.requestIdleCallback === undefined ||
navigator.scheduling?.isInputPending === undefined
) {
return new FallbackBackgroundScheduler()
}
return new BackgroundScheduler()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment