/** * 共享合作调度软预算,供导入、复制、恢复编码等逐项计算调用。 * 既按耗时判断,也按工作量兜底,避免粗粒度/倒退时钟让长任务一直不让出;单个不可分步骤仍由调用方限大小。 */ import {fault} from '../canvas-core/values.ts'; /** 注入时钟、让出任务与目标时间片;它们不改变记录语义或操作身份。 */ export type ImportScheduling = { now?: () => number; yieldTask?: () => Promise; frameBudgetMs?: number; }; /** A soft time slice with a work threshold, even when the clock is coarse. * The caller bounds each indivisible parse/decode operation separately. */ /** 默认 6ms 软目标,闭包保存本时间片 started/work/unchecked 三个计数,不存任务或文档。 * step 可能同步返回 undefined;只有返回 Promise 且调用方 await 时才形成有效背压,应串行使用同一预算。 * 自定义 yieldTask 决定让出到微任务还是后续任务;预算不保证浏览器一定在该点完成一帧绘制。 * 构造只校验 frameBudgetMs,不预先检查 signal;units 应由调用者提供非负有限工作量。 */ export function createImportBudget(signal: AbortSignal, options: ImportScheduling = {}) { const budget = options.frameBudgetMs ?? 6; if (!Number.isFinite(budget) || budget <= 0) throw fault('invalid_import_budget'); const now = options.now ?? (() => performance.now()); const yieldTask = options.yieldTask ?? (() => new Promise(resolve => setTimeout(resolve, 0))); // 每128工作单位才读一次时钟,减少高频开销;总量16384强制让出,不能因时间测不准而无限执行。 let started = now(), work = 0, unchecked = 0; return { /** 累加 units;未满 128 个未检查单位且总量未满 16384 时直接返回,连 signal 也尚未检查。 * 阈值到达才读时钟;未超时保留 work 累计,超时/时间倒退或非有限值/工作量封顶则 await yieldTask。 * yield 后复查取消,成功才重设 started/work;读取时钟或 yield 抛错直接传播,不隐式重试。 * units 无运行时校验,负数/NaN 会破坏计数假设;本函数不测量单次实际 CPU 成本或字节数。 */ step(units = 1): Promise | undefined { work += units; unchecked += units; if (unchecked < 128 && work < 16384) return; unchecked = 0; if (signal.aborted) throw fault('import_aborted'); const elapsed = now() - started; if (work < 16384 && elapsed >= 0 && elapsed < budget) return; // 这里的异步边界才实际交出执行权;预算是 cooperative,不能抢占前面已经运行的同步工作。 return (async () => { await yieldTask(); if (signal.aborted) throw fault('import_aborted'); started = now(); work = 0; })(); }, }; }