Skip to content

Instantly share code, notes, and snippets.

@schickling
Last active September 29, 2023 08:19
Show Gist options
  • Save schickling/0f6fcf105806168a160e43ac001f2d02 to your computer and use it in GitHub Desktop.
Save schickling/0f6fcf105806168a160e43ac001f2d02 to your computer and use it in GitHub Desktop.
// Adds React render profiling for our AppMeters
React.useEffect(() => {
// @ts-expect-error `__REACT_DEVTOOLS_GLOBAL_HOOK__` isn't typed yet
const devToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__
// For meaning of priority level see https://github.com/facebook/react/blob/eb2c2f7c2cf2652a168c2b433d2989131c69754b/fixtures/fiber-debugger/src/Fibers.js#L240
type PriorityLevel = 1 | 2 | 3 | 4 | 5
// Check if the hook is available and supports the Fiber reconciliation algorithm
if (devToolsHook && devToolsHook.supportsFiber) {
// Register the onCommitFiberRoot event
const previousOnCommitFiberRoot = devToolsHook.onCommitFiberRoot
devToolsHook.onCommitFiberRoot = (
id: number,
root: any,
maybePriorityLevel: PriorityLevel,
didError: boolean,
) => {
// console.log('onCommitFiberRoot', id, root, maybePriorityLevel, didError)
window[`__debugReactRender${maybePriorityLevel}`]++
window[`__debugReactRenderedThisFrame`] = true
window.requestAnimationFrame(() => {
window[`__debugReactRenderedThisFrame`] = false
})
previousOnCommitFiberRoot(id, root, maybePriorityLevel, didError)
}
}
}, [])
/* eslint-disable prefer-arrow/prefer-arrow-functions */
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import type { RuntimeFiber } from '@effect/io/Fiber'
import * as FiberRef from '@effect/io/FiberRef'
import * as Scheduler from '@effect/io/Scheduler'
export * from '@effect/io/Scheduler'
declare global {
interface Navigator {
scheduling:
| {
isInputPending: (() => boolean) | undefined
}
| undefined
}
function cancelAnimationFrame(handle: number | undefined): void
function cancelIdleCallback(handle: number | undefined): void
}
// Based on https://github.com/Effect-TS/io/blob/main/src/Scheduler.ts#L63
export class ReactAwareScheduler implements Scheduler.Scheduler {
private running = false
private tasks = new Array<Scheduler.Task>()
constructor(private maxNextTickBeforeTimer: number) {}
shouldYield(fiber: RuntimeFiber<unknown, unknown>): number | false {
return fiber.currentOpCount > fiber.getFiberRef(FiberRef.currentMaxOpsBeforeYield)
? fiber.getFiberRef(FiberRef.currentSchedulingPriority)
: false
}
scheduleTask(task: Scheduler.Task) {
this.tasks.push(task)
if (this.running === false) {
this.running = true
this.starve(0, Date.now())
}
}
private starveInternal(depth: number, starveStartedMs: number) {
const tasks = this.tasks
this.tasks = []
// TODO do some metering here to avoid frame drops
for (const task of tasks) {
task!()
}
if (this.tasks.length === 0) {
this.running = false
} else {
this.starve(depth, starveStartedMs)
}
}
private starve(executionDepth: number, starveStartedMs_: number) {
const { shouldYield, starveStartedMs } = this.shouldYieldToNextFrame(executionDepth, starveStartedMs_)
if (shouldYield) {
setTimeout(() => this.starveInternal(0, starveStartedMs), 0)
} else {
// Schedules remaining work as a microtask (using `Promise.resolve()` instead of `queueMicrotask()` for better compatibility (e.g. React Native))
void Promise.resolve(void 0).then(() => this.starveInternal(executionDepth + 1, starveStartedMs))
}
}
/** NOTE We're also returning `starveStartedMs` to avoid having to call` Date.now()` twice */
private shouldYieldToNextFrame(
depth: number,
starveStartedMs: number,
): { shouldYield: boolean; starveStartedMs: number } {
// @ts-expect-error TODO improve this (at least the typing)
const reactRenderedThisFrame = window.__debugReactRenderedThisFrame
if (reactRenderedThisFrame) return { shouldYield: true, starveStartedMs }
if (depth >= this.maxNextTickBeforeTimer) return { shouldYield: true, starveStartedMs }
const now = Date.now()
// TODO use actual FPS
if (now - starveStartedMs > 10) return { shouldYield: true, starveStartedMs: now }
return { shouldYield: false, starveStartedMs }
}
}
/** NOTE should only be used on Main Thread */
export class BackgroundScheduler implements Scheduler.Scheduler {
private running = false
private taskBuckets = new Scheduler.PriorityBuckets()
private idleDeadline: IdleDeadline | undefined
scheduleTask(task: Scheduler.Task, priority: number) {
this.taskBuckets.scheduleTask(task, priority)
if (this.running === false) {
this.running = true
this.starveWhenIdle()
}
}
shouldYield(fiber: RuntimeFiber<unknown, unknown>): number | false {
if (
// TODO re-enable once fixed https://github.com/Effect-TS/io/pull/678/files
// navigator.scheduling!.isInputPending!() ||
// // (this.idleDeadline !== undefined && this.idleDeadline.timeRemaining() <= 0.5) ||
fiber.currentOpCount > fiber.getFiberRef(FiberRef.currentMaxOpsBeforeYield)
) {
return fiber.getFiberRef(FiberRef.currentSchedulingPriority)
}
return false
}
private doWork() {
try {
while (true) {
const task = getNextTask(this.taskBuckets)
if (task === undefined) {
this.running = false
return
} else {
task()
if (navigator.scheduling!.isInputPending!() || this.idleDeadline!.timeRemaining() <= 0.5) {
this.starveWhenIdle()
return
}
}
}
} finally {
this.idleDeadline = undefined
}
}
private starveWhenIdle() {
requestIdleCallback((idleDeadline) => {
this.idleDeadline = idleDeadline
this.doWork()
})
}
}
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()
}
shouldYield(fiber: RuntimeFiber<unknown, unknown>): number | false {
return fiber.currentOpCount > fiber.getFiberRef(FiberRef.currentMaxOpsBeforeYield) || this.isTimeToYield()
? fiber.getFiberRef(FiberRef.currentSchedulingPriority)
: false
}
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.isTimeToYield()) {
this.starve()
return
}
}
}
}
private isTimeToYield() {
const now = Date.now()
const timeSinceLastFrame = now - this.lastWitnessedFrame
const timeRemaining = this.timePerFrame - timeSinceLastFrame
return timeRemaining <= 0.5
}
}
/**
* 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 reactAwareScheduler = (): Scheduler.Scheduler => new ReactAwareScheduler(2048)
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