Last active
September 29, 2023 08:19
-
-
Save schickling/0f6fcf105806168a160e43ac001f2d02 to your computer and use it in GitHub Desktop.
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
// 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) | |
} | |
} | |
}, []) |
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
/* 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