Skip to content

Commit

Permalink
♻️ SchedulingTask and ScheduledTask
Browse files Browse the repository at this point in the history
  • Loading branch information
astoilkov committed Feb 20, 2024
1 parent 367fd4a commit 980ad69
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 42 deletions.
8 changes: 4 additions & 4 deletions src/ScheduledTask.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import SchedulingStrategy from './SchedulingStrategy'
import { PromiseWithResolvers } from './utils/withResolvers'
import type SchedulingTask from './SchedulingTask'

type ScheduledTask = PromiseWithResolvers & {
strategy: SchedulingStrategy
type ScheduledTask = SchedulingTask & {
promise: Promise<void>
resolve: () => void
}

export default ScheduledTask
6 changes: 6 additions & 0 deletions src/SchedulingTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default interface SchedulingTask {
type: 'frame-based' | 'idle-based'
workTime: number
priority: number
signal?: AbortSignal
}
34 changes: 13 additions & 21 deletions src/ThreadScheduler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import ScheduledTask from './ScheduledTask'
import WorkCycleTracker from './WorkCycleTracker'
import SchedulingStrategy from './SchedulingStrategy'
import withResolvers from './utils/withResolvers'
import ReactiveTask from './utils/ReactiveTask'

const strategyPriorities = {
interactive: 30,
smooth: 20,
idle: 10,
}
import type ScheduledTask from './ScheduledTask'
import type SchedulingTask from './SchedulingTask'
import withResolvers from './utils/withResolvers'

class ThreadScheduler {
#tasks: ScheduledTask[] = []
Expand All @@ -27,21 +21,19 @@ class ThreadScheduler {
})
}

createTask(strategy: SchedulingStrategy): ScheduledTask {
const task = { ...withResolvers(), strategy }

this.#insertTask(task)

return task
schedule(task: SchedulingTask): ScheduledTask {
const scheduled = { ...task, ...withResolvers() }
this.#insertTask(scheduled)
return scheduled
}

isTimeToYield(strategy: SchedulingStrategy): boolean {
return !this.#workCycleTracker.canWorkMore(strategy)
isTimeToYield(task: SchedulingTask): boolean {
return !this.#workCycleTracker.canWorkMore(task)
}

async #completeTask(task: ScheduledTask, signal: AbortSignal): Promise<void> {
while (!this.#workCycleTracker.canWorkMore(task.strategy)) {
await this.#workCycleTracker.nextWorkCycle(task.strategy)
while (!this.#workCycleTracker.canWorkMore(task)) {
await this.#workCycleTracker.nextWorkCycle(task)

if (signal.aborted) {
return
Expand All @@ -66,9 +58,9 @@ class ThreadScheduler {
}

#insertTask(task: ScheduledTask): void {
const priority = strategyPriorities[task.strategy]
const priority = task.priority
for (let i = 0; i < this.#tasks.length; i++) {
if (priority >= strategyPriorities[this.#tasks[i]!.strategy]) {
if (priority >= this.#tasks[i]!.priority) {
this.#tasks.splice(i, 0, task)
this.#topTask.set(this.#tasks[0])
return
Expand Down
28 changes: 15 additions & 13 deletions src/WorkCycleTracker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ricTracker from './ricTracker'
import frameTracker from './frameTracker'
import SchedulingStrategy from './SchedulingStrategy'
import waitHiddenTask from './utils/waitHiddenTask'
import type SchedulingTask from './SchedulingTask'

export default class WorkCycleTracker {
#workCycleStart: number = -1
Expand All @@ -16,15 +16,15 @@ export default class WorkCycleTracker {
frameTracker.requestStop()
}

canWorkMore(strategy: SchedulingStrategy): boolean {
canWorkMore(task: SchedulingTask): boolean {
const isInputPending = navigator.scheduling?.isInputPending?.() === true
return !isInputPending && this.#calculateDeadline(strategy) - Date.now() > 0
return !isInputPending && this.#calculateDeadline(task) - Date.now() > 0
}

async nextWorkCycle(strategy: SchedulingStrategy): Promise<void> {
if (strategy === 'interactive' || strategy === 'smooth') {
async nextWorkCycle(task: SchedulingTask): Promise<void> {
if (task.type === 'frame-based') {
await Promise.race([frameTracker.waitAfterFrame(), waitHiddenTask()])
} else if (strategy === 'idle') {
} else if (task.type === 'idle-based') {
if (ricTracker.available) {
await ricTracker.waitIdleCallback()
} else {
Expand All @@ -36,17 +36,19 @@ export default class WorkCycleTracker {
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') {
#calculateDeadline(task: SchedulingTask): number {
if (task.type === 'frame-based') {
// const timePerFrame = 1000 / fps.guessRefreshRate()
// const multiplier = timePerFrame / fps.guessRefreshRate()
// const maxWorkTime = fps.fps() * multiplier
// return this.#workCycleStart + maxWorkTime
return this.#workCycleStart + task.workTime
} else if (task.type === 'idle-based') {
const idleDeadline =
ricTracker.deadline === undefined
? Number.MAX_SAFE_INTEGER
: Date.now() + ricTracker.deadline.timeRemaining()
return Math.min(this.#workCycleStart + 5, idleDeadline)
return Math.min(this.#workCycleStart + task.workTime, idleDeadline)
}
return -1
}
Expand Down
3 changes: 2 additions & 1 deletion src/isTimeToYield.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hasValidContext from './utils/hasValidContext'
import SchedulingStrategy from './SchedulingStrategy'
import threadScheduler from './ThreadScheduler'
import strategyToTask from './utils/strategyToTask'

// #performance
// calling `isTimeToYield()` thousand of times is slow
Expand Down Expand Up @@ -33,7 +34,7 @@ export default function isTimeToYield(strategy: SchedulingStrategy = 'smooth'):
}

cache.lastCallTime = now
cache.lastResult = threadScheduler.isTimeToYield(strategy)
cache.lastResult = threadScheduler.isTimeToYield(strategyToTask(strategy))

return cache.lastResult
}
2 changes: 1 addition & 1 deletion src/utils/ReactiveTask.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ScheduledTask from '../ScheduledTask'
import type ScheduledTask from '../ScheduledTask'

// - reactivity for ScheduledTask
// - otherwise, we would have to use something heavier like solid-js
Expand Down
23 changes: 23 additions & 0 deletions src/utils/strategyToTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type SchedulingStrategy from '../SchedulingStrategy'
import type SchedulingTask from '../SchedulingTask'

export default function strategyToTask(schedulingStrategy: SchedulingStrategy): SchedulingTask {
const options: Record<SchedulingStrategy, SchedulingTask> = {
interactive: {
type: 'frame-based',
workTime: 83,
priority: 30,
},
smooth: {
type: 'frame-based',
workTime: 13,
priority: 20,
},
idle: {
type: 'idle-based',
workTime: 5,
priority: 10,
},
}
return options[schedulingStrategy]
}
4 changes: 2 additions & 2 deletions src/yieldControl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hasValidContext from './utils/hasValidContext'
import SchedulingStrategy from './SchedulingStrategy'
import threadScheduler from './ThreadScheduler'
import strategyToTask from './utils/strategyToTask'

/**
* Waits for the browser to become idle again in order to resume work. Calling `yieldControl()`
Expand All @@ -17,6 +18,5 @@ export default async function yieldControl(strategy: SchedulingStrategy = 'smoot
return
}

const task = threadScheduler.createTask(strategy)
return task.promise
return threadScheduler.schedule(strategyToTask(strategy)).promise
}

0 comments on commit 980ad69

Please sign in to comment.