blob: cadae2d53128fd2b52ce70e628eb8704b931db15 [file] [log] [blame] [edit]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
type Priority = 'user-blocking' | 'user-visible' | 'background';
// Type declaration for the Scheduler API (not yet in TypeScript's lib.dom.d.ts)
declare global {
interface Scheduler {
yield(): Promise<void>;
postTask(
callback: (args: void) => void,
options?: {priority?: Priority},
): Promise<void>;
}
// eslint-disable-next-line no-var
var scheduler: Scheduler | undefined;
}
// Polyfill for scheduler.postTask()
export function postTask(
callback: (args: void) => void,
options?: {priority?: Priority},
): void {
if (globalThis.scheduler?.postTask) {
globalThis.scheduler.postTask(callback, options);
} else {
setTimeout(() => callback(), 0);
}
}
// Polyfill for scheduler.yield()
export function yieldTask(): Promise<void> {
if (globalThis.scheduler?.yield) {
return globalThis.scheduler.yield();
} else {
return new Promise<void>((r) => setTimeout(() => r(), 0));
}
}
export interface ChunkedTaskContext {
readonly shouldYield: () => boolean;
readonly yield: () => Promise<void>;
}
export interface ChunkedTaskOptions {
readonly priority?: Priority;
readonly workBudgetMs?: number;
}
// 4ms is a reasonable default budget
const DEFAULT_WORK_BUDGET_MS = 4;
/**
* Returns a promise that resolves in a new task with the configured priority.
* It returns a task context that can be used to cooperatively yield back to the
* event loop after a certain work budget (in ms) has been exhausted.
*
* This helps create long-running tasks that do not block the main thread for
* too long or hold up frames for too long.
*
* Uses the Scheduler API if available, otherwise falls back to setTimeout().
*
* The default priority is 'user-visible' and the default work budget is 4ms.
*
* Usage:
* const task = await deferChunkedTask({priority: 'user-visible', workBudgetMs: 5});
* // Now running in a new task...
* for (let i = 0; i < bigNumber; i++) {
* // do work...
* if (task.shouldYield()) {
* await task.yield();
* }
* }
*/
export async function deferChunkedTask(
opts: ChunkedTaskOptions = {},
): Promise<ChunkedTaskContext> {
const {priority, workBudgetMs = DEFAULT_WORK_BUDGET_MS} = opts;
return await new Promise<ChunkedTaskContext>((res) => {
postTask(
() => {
res(createChunkedTaskContext(workBudgetMs));
},
{priority},
);
});
}
function createChunkedTaskContext(workBudgetMs: number): ChunkedTaskContext {
let deadline = performance.now() + workBudgetMs;
return {
shouldYield: () => {
const timeRemaining = deadline - performance.now();
return timeRemaining <= 0;
},
yield: async () => {
await yieldTask();
// Reset the deadline after yielding
deadline = performance.now() + workBudgetMs;
},
};
}