-
-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
this rewrite will allow us to add new features easily and make the code easier to understand
- Loading branch information
Showing
19 changed files
with
311 additions
and
359 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import SchedulingStrategy from './SchedulingStrategy' | ||
import { PromiseWithResolvers } from './utils/withResolvers' | ||
|
||
type ScheduledTask = PromiseWithResolvers & { | ||
strategy: SchedulingStrategy | ||
} | ||
|
||
export default ScheduledTask |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import ScheduledTask from './ScheduledTask' | ||
import WorkCycleTracker from './WorkCycleTracker' | ||
import SchedulingStrategy from './SchedulingStrategy' | ||
import withResolvers from './utils/withResolvers' | ||
import { requestPromiseEscape } from './utils/promiseEscape' | ||
import ReactiveTask from './utils/ReactiveTask' | ||
|
||
const strategyPriorities = { | ||
interactive: 0, | ||
smooth: 1, | ||
idle: 2, | ||
} | ||
|
||
class Scheduler { | ||
#tasks: ScheduledTask[] = [] | ||
#topTask: ReactiveTask = new ReactiveTask() | ||
#workCycleTracker = new WorkCycleTracker() | ||
|
||
constructor() { | ||
this.#topTask.setEffect(async (task, signal) => { | ||
this.#workCycleTracker.startTracking() | ||
|
||
await this.#completeTask(task, signal) | ||
|
||
if (this.#tasks.length === 0) { | ||
this.#workCycleTracker.requestStopTracking() | ||
} | ||
}) | ||
} | ||
|
||
createTask(strategy: SchedulingStrategy): ScheduledTask { | ||
const task = { ...withResolvers(), strategy } | ||
|
||
this.#insertTask(task) | ||
|
||
return task | ||
} | ||
|
||
isTimeToYield(strategy: SchedulingStrategy): boolean { | ||
return !this.#workCycleTracker.canWorkMore(strategy) | ||
} | ||
|
||
async #completeTask(task: ScheduledTask, signal: AbortSignal): Promise<void> { | ||
while (!this.#workCycleTracker.canWorkMore(task.strategy)) { | ||
await this.#workCycleTracker.nextWorkCycle(task.strategy) | ||
|
||
if (signal.aborted) { | ||
return | ||
} | ||
} | ||
|
||
task.resolve() | ||
|
||
// wait for the user code to continue running the code to see if he will add more work to | ||
// be done. we prefer this, other than continuing to the next task immediately | ||
await new Promise<void>((resolve) => requestPromiseEscape(resolve)) | ||
|
||
this.#removeTask(task) | ||
} | ||
|
||
#insertTask(task: ScheduledTask): void { | ||
const priority = strategyPriorities[task.strategy] | ||
for (let i = 0; i < this.#tasks.length; i++) { | ||
if (priority >= strategyPriorities[this.#tasks[i]!.strategy]) { | ||
this.#tasks.splice(i, 0, task) | ||
this.#topTask.set(this.#tasks[0]) | ||
return | ||
} | ||
} | ||
this.#tasks.push(task) | ||
this.#topTask.set(this.#tasks[0]) | ||
} | ||
|
||
#removeTask(task: ScheduledTask): void { | ||
const index = this.#tasks.indexOf(task) | ||
if (index !== -1) { | ||
this.#tasks.splice(index, 1) | ||
} | ||
|
||
this.#topTask.set(this.#tasks[0]) | ||
} | ||
} | ||
|
||
const scheduler = new Scheduler() | ||
|
||
export default scheduler |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import ricTracker from './ricTracker' | ||
import frameTracker from './frameTracker' | ||
import SchedulingStrategy from './SchedulingStrategy' | ||
|
||
export default class WorkCycleTracker { | ||
#workCycleStart: number = -1 | ||
|
||
startTracking() { | ||
ricTracker.start() | ||
frameTracker.start() | ||
} | ||
|
||
requestStopTracking() { | ||
ricTracker.stop() | ||
frameTracker.requestStop() | ||
} | ||
|
||
canWorkMore(strategy: SchedulingStrategy): boolean { | ||
const isInputPending = navigator.scheduling?.isInputPending?.() === true | ||
return !isInputPending && this.#calculateDeadline(strategy) - Date.now() > 0 | ||
} | ||
|
||
async nextWorkCycle(strategy: SchedulingStrategy) { | ||
if (strategy === 'interactive') { | ||
await frameTracker.waitAfterFrame() | ||
} else if (strategy === 'smooth') { | ||
await frameTracker.waitAfterFrame() | ||
} else if (strategy === 'idle') { | ||
if (ricTracker.available) { | ||
await ricTracker.waitIdleCallback() | ||
} else { | ||
await frameTracker.waitAnimationFrame() | ||
} | ||
} | ||
|
||
this.#workCycleStart = Date.now() | ||
} | ||
|
||
#calculateDeadline(strategy: SchedulingStrategy): number { | ||
if (strategy === 'interactive') { | ||
return this.#workCycleStart + 83 | ||
} else if (strategy === 'smooth') { | ||
return this.#workCycleStart + 13 | ||
} else if (strategy === 'idle') { | ||
const idleDeadline = | ||
ricTracker.deadline === undefined | ||
? Number.MAX_SAFE_INTEGER | ||
: Date.now() + ricTracker.deadline.timeRemaining() | ||
return Math.min(this.#workCycleStart + 5, idleDeadline) | ||
} | ||
return -1 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import withResolvers from './utils/withResolvers' | ||
import { queueTask } from '../index' | ||
|
||
class FrameTracker { | ||
#resolve: () => void | ||
#promise: Promise<void> | ||
#timeoutId?: number | ||
#requestAnimationId?: number | ||
|
||
constructor() { | ||
const { promise, resolve } = withResolvers() | ||
this.#promise = promise | ||
this.#resolve = resolve | ||
} | ||
|
||
async waitAnimationFrame(): Promise<void> { | ||
return this.#promise | ||
} | ||
|
||
async waitAfterFrame(): Promise<void> { | ||
await this.#promise | ||
await new Promise<void>((resolve) => queueTask(resolve)) | ||
} | ||
|
||
start(): void { | ||
if (this.#requestAnimationId !== undefined) { | ||
return | ||
} | ||
|
||
this.#loop() | ||
clearTimeout(this.#timeoutId) | ||
|
||
this.#timeoutId = undefined | ||
} | ||
|
||
requestStop(): void { | ||
if (this.#timeoutId === undefined) { | ||
this.#timeoutId = setTimeout(() => { | ||
this.#timeoutId = undefined | ||
if (this.#requestAnimationId !== undefined) { | ||
cancelAnimationFrame(this.#requestAnimationId) | ||
} | ||
}, 200) | ||
} | ||
} | ||
|
||
#loop(): void { | ||
this.#requestAnimationId = requestAnimationFrame(() => { | ||
this.#resolve() | ||
|
||
const { promise, resolve } = withResolvers() | ||
this.#promise = promise | ||
this.#resolve = resolve | ||
|
||
this.#loop() | ||
}) | ||
} | ||
} | ||
|
||
const frameTracker = new FrameTracker() | ||
|
||
export default frameTracker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import withResolvers from './utils/withResolvers' | ||
|
||
class RicTracker { | ||
#promise: Promise<IdleDeadline> | ||
#resolve: (deadline: IdleDeadline) => void | ||
#idleCallbackId?: number | ||
#idleDeadline?: IdleDeadline | ||
|
||
constructor() { | ||
const { promise, resolve } = withResolvers<IdleDeadline>() | ||
this.#promise = promise | ||
this.#resolve = resolve | ||
} | ||
|
||
get available() { | ||
return typeof requestIdleCallback !== 'undefined' | ||
} | ||
|
||
get deadline(): IdleDeadline | undefined { | ||
return this.#idleDeadline | ||
} | ||
|
||
async waitIdleCallback(): Promise<IdleDeadline> { | ||
return this.#promise | ||
} | ||
|
||
start(): void { | ||
if (!this.available || this.#idleCallbackId !== undefined) { | ||
return | ||
} | ||
|
||
this.#idleCallbackId = requestIdleCallback((deadline) => { | ||
this.#idleDeadline = deadline | ||
this.#idleCallbackId = undefined | ||
|
||
this.#resolve?.(deadline) | ||
|
||
const { promise, resolve } = withResolvers<IdleDeadline>() | ||
this.#promise = promise | ||
this.#resolve = resolve | ||
}) | ||
} | ||
|
||
stop() { | ||
if (this.#idleCallbackId !== undefined) { | ||
cancelIdleCallback(this.#idleCallbackId) | ||
} | ||
} | ||
} | ||
|
||
const ricTracker = new RicTracker() | ||
|
||
export default ricTracker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.