Skip to content

Commit

Permalink
♻️ SchedulingPrioritySchedulingStrategy and new names for the …
Browse files Browse the repository at this point in the history
…strategies
  • Loading branch information
astoilkov committed Jan 17, 2024
1 parent f63de1e commit 9ceb119
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 75 deletions.
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export { default as yieldOrContinue } from './src/yieldOrContinue'
// secondary
export { default as yieldControl } from './src/yieldControl'
export { default as isTimeToYield } from './src/isTimeToYield'
export type { default as SchedulingPriority } from './src/SchedulingPriority'
export type { default as SchedulingPriority } from './src/SchedulingStrategy'

// utility
export { default as queueTask } from './src/utils/queueTask'
Expand Down
6 changes: 3 additions & 3 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
<button id="run-all">run all</button>
</div>
<div>
<button id="post-task-blocking">postTask('user-blocking')</button>
<button id="post-task-visible">postTask('user-visible')</button>
<button id="post-task-background">postTask('background')</button>
<button id="post-task-blocking">postTask('interactive')</button>
<button id="post-task-visible">postTask('smooth')</button>
<button id="post-task-background">postTask('idle')</button>
</div>
<div>
<span id="animation">animation</span>
Expand Down
18 changes: 9 additions & 9 deletions playground/playground.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { isTimeToYield, SchedulingPriority, yieldOrContinue } from '../index'

document.querySelector('#run-user-visible')!.addEventListener('click', () => {
run('user-visible')
run('smooth')
})
document.querySelector('#run-user-blocking')!.addEventListener('click', () => {
run('user-blocking')
run('interactive')
})
document.querySelector('#run-background')!.addEventListener('click', () => {
run('background')
run('idle')
})
document.querySelector('#run-all')!.addEventListener('click', async () => {
await run('user-blocking')
await run('user-visible')
await run('background')
await run('interactive')
await run('smooth')
await run('idle')
})

async function run(priority: SchedulingPriority) {
Expand All @@ -25,13 +25,13 @@ async function run(priority: SchedulingPriority) {
}

document.querySelector('#post-task-blocking')!.addEventListener('click', () => {
runPostTask('user-blocking')
runPostTask('interactive')
})
document.querySelector('#post-task-visible')!.addEventListener('click', () => {
runPostTask('user-visible')
runPostTask('smooth')
})
document.querySelector('#post-task-background')!.addEventListener('click', () => {
runPostTask('background')
runPostTask('idle')
})

async function runPostTask(priority: SchedulingPriority) {
Expand Down
16 changes: 8 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ You can see the library in action in [this CodeSandbox](https://codesandbox.io/s

## API

#### `yieldOrContinue(priority: 'user-blocking' | 'user-visible' | 'background')`
#### `yieldOrContinue(priority: 'interactive' | 'smooth' | 'idle')`

The complexity of the entire library is hidden behind this method. You can have great app performance by calling a single method.

```ts
async function findInFiles(query: string) {
for (const file of files) {
await yieldOrContinue('user-blocking')
await yieldOrContinue('interactive')

for (const line of file.lines) {
fuzzySearchLine(line, query)
Expand All @@ -95,18 +95,18 @@ _This is a utility function, most people don't need to use it._ The same way `qu
### More complex scenarios

The library has two more functions available:
- `yieldControl(priority: 'user-blocking' | 'user-visible' | 'background')`
- `isTimeToYield(priority: 'user-blocking' | 'user-visible' | 'background')`
- `yieldControl(priority: 'interactive' | 'smooth' | 'idle')`
- `isTimeToYield(priority: 'interactive' | 'smooth' | 'idle')`

These two functions are used together to handle more advanced use cases.

A simple use case where you will need those two functions is when you want to render your view before yielding back control to the browser to continue its work:
```ts
async function doHeavyWork() {
for (const value of values) {
if (isTimeToYield('user-blocking')) {
if (isTimeToYield('interactive')) {
render()
await yieldControl('user-blocking')
await yieldControl('interactive')
}

computeHeavyWorkOnValue(value)
Expand Down Expand Up @@ -134,7 +134,7 @@ If you want the benefits of `main-thread-scheduling`, but you prefer the `postTa
async function postTask(callback: () => void | Promise<void>, options?: {
priority: SchedulingPriority
}) {
await yieldOrContinue(options?.priority ?? 'user-visible')
await yieldOrContinue(options?.priority ?? 'smooth')
await callback()
}
```
Expand Down Expand Up @@ -165,4 +165,4 @@ In-depth overview for some of the concepts talked in the document:
- [JavaScript Event Loop vs Node JS Event Loop](https://blog.insiderattack.net/javascript-event-loop-vs-node-js-event-loop-aea2b1b85f5c)
- [Using `requestIdleCallback()`](https://developers.google.com/web/updates/2015/08/using-requestidlecallback)
-->
-->
3 changes: 0 additions & 3 deletions src/SchedulingPriority.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/SchedulingStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// It schedules the task while keeping the page interactive/smooth/idle.
type SchedulingStrategy = 'interactive' | 'smooth' | 'idle'

export default SchedulingStrategy
16 changes: 8 additions & 8 deletions src/isTimeToYield.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import schedulingState from './schedulingState'
import hasValidContext from './utils/hasValidContext'
import SchedulingPriority from './SchedulingPriority'
import SchedulingStrategy from './SchedulingStrategy'

// #performance
// calling `isTimeToYield()` thousand of times is slow
Expand All @@ -14,7 +14,7 @@ const cache = {
/**
* Determines if it's time to call `yieldControl()`.
*/
export default function isTimeToYield(priority: SchedulingPriority = 'user-visible'): boolean {
export default function isTimeToYield(priority: SchedulingStrategy = 'smooth'): boolean {
if (cache.hasValidContext === undefined) {
cache.hasValidContext = hasValidContext()
}
Expand Down Expand Up @@ -43,24 +43,24 @@ export default function isTimeToYield(priority: SchedulingPriority = 'user-visib
return cache.lastResult
}

function calculateDeadline(priority: SchedulingPriority): number {
function calculateDeadline(priority: SchedulingStrategy): number {
if (schedulingState.thisFrameWorkStartTime === undefined) {
return -1
}

switch (priority) {
case 'user-blocking': {
// spent the max recommended 100ms doing 'user-blocking' tasks minus 1 frame (16ms):
case 'interactive': {
// spent the max recommended 100ms doing 'interactive' tasks minus 1 frame (16ms):
// - https://developer.mozilla.org/en-US/docs/Web/Performance/How_long_is_too_long#responsiveness_goal
// - Math.round(100 - (1000/60)) = Math.round(83,333) = 83
return schedulingState.thisFrameWorkStartTime + 83
}
case 'user-visible': {
// spent 80% percent of the frame's budget running 'user-visible' tasks:
case 'smooth': {
// spent 80% percent of the frame's budget running 'smooth' tasks:
// - Math.round((1000/60) * 0.8) = Math.round(13,333) = 13
return schedulingState.thisFrameWorkStartTime + 13
}
case 'background': {
case 'idle': {
const idleDeadline =
schedulingState.idleDeadline === undefined
? Number.MAX_SAFE_INTEGER
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/ScheduledTask.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import SchedulingPriority from '../SchedulingPriority'
import SchedulingStrategy from '../SchedulingStrategy'
import { PromiseWithResolvers } from '../utils/withResolvers'

type ScheduledTask = PromiseWithResolvers & {
priority: SchedulingPriority
priority: SchedulingStrategy
}

export default ScheduledTask
14 changes: 7 additions & 7 deletions src/tasks/createTask.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import SchedulingPriority from '../SchedulingPriority'
import SchedulingStrategy from '../SchedulingStrategy'
import ScheduledTask from './ScheduledTask'
import withResolvers from '../utils/withResolvers'
import schedulingState from '../schedulingState'
import { startTracking } from '../tracking'

/**
* Adds a task to the queue and returns the new task.
* @param priority {SchedulingPriority} The priority of the new task.
* @param priority {SchedulingStrategy} The priority of the new task.
*/
export default function createTask(priority: SchedulingPriority): ScheduledTask {
export default function createTask(priority: SchedulingStrategy): ScheduledTask {
const item = { ...withResolvers(), priority }
const insertIndex =
priority === 'user-blocking'
priority === 'interactive'
? 0
: priority === 'user-visible'
: priority === 'smooth'
? schedulingState.tasks.findIndex(
(task) => task.priority === 'user-visible' || task.priority === 'background',
(task) => task.priority === 'smooth' || task.priority === 'idle',
)
: schedulingState.tasks.findIndex((task) => task.priority === 'background')
: schedulingState.tasks.findIndex((task) => task.priority === 'idle')

if (insertIndex === -1) {
schedulingState.tasks.push(item)
Expand Down
14 changes: 6 additions & 8 deletions src/yieldControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import schedulingState from './schedulingState'
import queueTask from './utils/queueTask'
import isTimeToYield from './isTimeToYield'
import hasValidContext from './utils/hasValidContext'
import SchedulingPriority from './SchedulingPriority'
import SchedulingStrategy from './SchedulingStrategy'
import { cancelPromiseEscape, requestPromiseEscape } from './utils/promiseEscape'
import createTask from './tasks/createTask'
import removeTask from './tasks/removeTask'
Expand All @@ -15,14 +15,12 @@ let promiseEscapeId: number | undefined
* multiple times will create a LIFO(last in, first out) queue – the last call to
* `yieldControl()` will get resolved first.
*
* @param priority {SchedulingPriority} The priority of the task being run.
* @param priority {SchedulingStrategy} The priority of the task being run.
* `user-visible` priority will always be resolved first. `background` priority will always be
* resolved second.
* @returns {Promise<void>} A promise that gets resolved when the work can continue.
*/
export default async function yieldControl(
priority: SchedulingPriority = 'user-visible',
): Promise<void> {
export default async function yieldControl(priority: SchedulingStrategy = 'smooth'): Promise<void> {
if (!hasValidContext()) {
return
}
Expand Down Expand Up @@ -50,14 +48,14 @@ export default async function yieldControl(
})
}

async function schedule(priority: SchedulingPriority): Promise<void> {
async function schedule(priority: SchedulingStrategy): Promise<void> {
if (schedulingState.isThisFrameBudgetSpent) {
await schedulingState.onAnimationFrame.promise
}

if (
priority === 'user-visible' ||
priority === 'user-blocking' ||
priority === 'smooth' ||
priority === 'interactive' ||
typeof requestIdleCallback === 'undefined'
) {
await new Promise<void>((resolve) => queueTask(resolve))
Expand Down
8 changes: 3 additions & 5 deletions src/yieldOrContinue.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import yieldControl from './yieldControl'
import isTimeToYield from './isTimeToYield'
import SchedulingPriority from './SchedulingPriority'
import SchedulingStrategy from './SchedulingStrategy'

/**
* If there is more time left — immediately returns so the task can continue. If no more time left,
* it waits for the browser to become idle again in order to resume work. Calling
* `yieldOrContinue()` multiple times will create a LIFO(last in, first out) queue – the last call
* to `yieldOrContinue()` will get resolved first.
*
* @param priority {SchedulingPriority} The priority of the task being run.
* @param priority {SchedulingStrategy} The priority of the task being run.
* `user-visible` priority will always be resolved first. `background` priority will always be
* resolved second.
* @returns {Promise<void>} A promise either immediately resolved or when the browser is ready to
* do work again.
*/
// disabling ESLint otherwise `requestPromiseEscape()` in `yieldControl()` won't work
// eslint-disable-next-line @typescript-eslint/promise-function-async
export default function yieldOrContinue(
priority: SchedulingPriority = 'user-visible',
): Promise<void> {
export default function yieldOrContinue(priority: SchedulingStrategy = 'smooth'): Promise<void> {
if (isTimeToYield(priority)) {
return yieldControl(priority)
}
Expand Down
42 changes: 21 additions & 21 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,28 @@ describe('main-thread-scheduling', () => {
;(window as any).MessageChannel = undefined
})

test(`isTimeToYield('user-visible') is true by default`, () => {
expect(isTimeToYieldMocked('user-visible')).toBe(true)
test(`isTimeToYield('smooth') is true by default`, () => {
expect(isTimeToYieldMocked('smooth')).toBe(true)
})

test(`yieldControl('user-visible')`, async () => {
await yieldControl('user-visible')
test(`yieldControl('smooth')`, async () => {
await yieldControl('smooth')

expect(isTimeToYieldMocked('user-visible')).toBe(false)
expect(isTimeToYieldMocked('smooth')).toBe(false)
})

test(`yieldOrContinue('user-visible') in a loop`, async () => {
test(`yieldOrContinue('smooth') in a loop`, async () => {
const now = Date.now()

while (Date.now() - now < 20) {
await yieldOrContinue('user-visible')
await yieldOrContinue('smooth')
}
})

test(`yieldControl('background')`, async () => {
await yieldControl('background')
test(`yieldControl('idle')`, async () => {
await yieldControl('idle')

expect(isTimeToYieldMocked('background')).toBe(false)
expect(isTimeToYieldMocked('idle')).toBe(false)
})

describe('with requestIdleCallback() mock', () => {
Expand Down Expand Up @@ -65,21 +65,21 @@ describe('main-thread-scheduling', () => {
;(window as any).cancelIdleCallback = undefined
})

test(`isTimeToYield('background') is true by default`, () => {
expect(isTimeToYieldMocked('background')).toBe(true)
test(`isTimeToYield('idle') is true by default`, () => {
expect(isTimeToYieldMocked('idle')).toBe(true)
})

test(`yieldControl('background')`, async () => {
await yieldControl('background')
test(`yieldControl('idle')`, async () => {
await yieldControl('idle')

expect(isTimeToYieldMocked('background')).toBe(false)
expect(isTimeToYieldMocked('idle')).toBe(false)
})

test(`yieldOrContinue('background') in a loop`, async () => {
test(`yieldOrContinue('idle') in a loop`, async () => {
const now = Date.now()

while (Date.now() - now < 20) {
await yieldOrContinue('background')
await yieldOrContinue('idle')
}
})

Expand All @@ -88,7 +88,7 @@ describe('main-thread-scheduling', () => {
isInputPending: () => true,
}

await Promise.all([yieldControl('background'), yieldControl('background')])
await Promise.all([yieldControl('idle'), yieldControl('idle')])
;(navigator as any).scheduling = undefined
})
})
Expand All @@ -108,11 +108,11 @@ describe('main-thread-scheduling', () => {
})

test(`isTimeToYield() returns true when isInputPending() returns true`, async () => {
await yieldOrContinue('user-visible')
await yieldOrContinue('smooth')

isInputPending = true

expect(isTimeToYieldMocked('user-visible')).toBe(true)
expect(isTimeToYieldMocked('smooth')).toBe(true)
})
})

Expand All @@ -139,7 +139,7 @@ async function watest(milliseconds: number): Promise<void> {
return await new Promise<void>((resolve) => setTimeout(resolve, milliseconds))
}

function isTimeToYieldMocked(priority: 'background' | 'user-visible'): boolean {
function isTimeToYieldMocked(priority: 'idle' | 'smooth'): boolean {
const originalDateNow = Date.now

Date.now = () => Math.random()
Expand Down

0 comments on commit 9ceb119

Please sign in to comment.