docs(runtime): explain import scheduling and dependency selection
This commit is contained in:
@@ -5,12 +5,15 @@
|
||||
import {fault} from '../canvas-core/values.ts';
|
||||
import {createImportBudget,type ImportScheduling} from './import-budget.ts';
|
||||
/** Share import scheduling while retaining the copy cancellation/error contract. */
|
||||
/** 验证copy预算后包装ImportBudget;调用者只有真正await返回Promise才会交出执行权。 */
|
||||
/** 复制任务的 ImportBudget 包装:构造与每次 step 都先检查取消,错误码使用 copy_aborted。
|
||||
* 只预校验 frameBudgetMs;units/时钟/调度器沿用底层合同,单位可以是记录数或由调用方折算的字节工作量。
|
||||
* 与同一个包装交互应顺序 await;没有独立计数器、持久进度或强制抢占能力。 */
|
||||
export function createCopyBudget(signal:AbortSignal,options:ImportScheduling={}){
|
||||
if(signal.aborted)throw fault('copy_aborted');
|
||||
if(options.frameBudgetMs!==undefined&&(!Number.isFinite(options.frameBudgetMs)||options.frameBudgetMs<=0))throw fault('invalid_copy_budget');
|
||||
const budget=createImportBudget(signal,options);
|
||||
/** signal已取消时将同步/异步预算失败统一为copy_aborted,其它错误原样抛给宿主。 */
|
||||
/** 只在处理错误时 signal 已取消才改成 copy_aborted;其它错误保留原对象。
|
||||
* 因此取消和宿主调度失败并发出现时,取消优先,不一定暴露原始调度错误。 */
|
||||
const fail=(error:unknown):never=>{if(signal.aborted)throw fault('copy_aborted');throw error;};
|
||||
/** 每次step先查copy取消,保持底层同步返回undefined或异步让出的原调度语义。 */
|
||||
return Object.freeze({step(units=1):Promise<void>|undefined{
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
*/
|
||||
/** Runner-owned feedback, not persisted job state. Probe bounded batches to
|
||||
* distinguish fixed publication cost from the marginal cost of another unit. */
|
||||
/** budget/max由runner给出,用实际已提交units和耗时更新下一批软上限。 */
|
||||
/** runner 提供已验证的正 budget/max,并在尝试后反馈 units/elapsed;本函数不自行读取时钟或发布内容。
|
||||
* 闭包只保留 limit 和 observed,重启 runner 会重新从 1 探测,不用持久化这些性能估计。
|
||||
* 参数/样本未在此验证,调用方需传有限且合理的耗时/单位;它不是容量或源完整性校验器。 */
|
||||
export function createImportBatches(budget:number,max:number){
|
||||
// 初始未观测时保守为1;observed只表示收到反馈,不是源校验或导入已完成。
|
||||
let limit=1,observed=false;
|
||||
@@ -13,7 +15,9 @@ export function createImportBatches(budget:number,max:number){
|
||||
get limit(){return limit;},
|
||||
/** 是否已有至少一次发布成本样本,用于runner的调度决策。 */
|
||||
get observed(){return observed;},
|
||||
/** 超时按比例减小并至少降一档;不足当前limit的尾批不能证明更大批也便宜。 */
|
||||
/** 首次反馈即 observed=true;elapsed > budget 按比例缩小,且尽量至少减 1,最低保留 1。
|
||||
* 未超时且 units ≥ limit 才按约 1.5 倍增长并封顶 max;等于 budget 也算此分支。
|
||||
* 不足当前 limit 的尾批不增长,避免将少量记录的固定发布成本误当成更大批仍便宜。 */
|
||||
record(units:number,elapsed:number){
|
||||
observed=true;
|
||||
if(elapsed>budget){limit=Math.max(1,Math.min(limit-1,Math.floor(units*budget/elapsed)));return;}
|
||||
|
||||
@@ -13,7 +13,10 @@ export type ImportScheduling = {
|
||||
|
||||
/** A soft time slice with a work threshold, even when the clock is coarse.
|
||||
* The caller bounds each indivisible parse/decode operation separately. */
|
||||
/** 默认6ms软目标;step 不一定产生 Promise,调用方须在返回等待值时真正 await/yield。 */
|
||||
/** 默认 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');
|
||||
@@ -22,7 +25,10 @@ export function createImportBudget(signal: AbortSignal, options: ImportSchedulin
|
||||
// 每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<void> | undefined {
|
||||
work += units;
|
||||
unchecked += units;
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
import type { ImportLocator } from './imports.ts';
|
||||
import { fault } from '../canvas-core/values.ts';
|
||||
|
||||
/** Ready SCCs are indivisible. Ordinary records retain source-order FIFO selection. */
|
||||
/** pending按源顺序输入;返回有序原子组,不实际提交,缺依赖的组继续等待而非直接丢失。 */
|
||||
/** Ready SCCs are indivisible; source order guides each readiness scan. */
|
||||
/** pending 由调用方按源顺序提供且 key 唯一;先找 SCC,再按组内/组间最早源索引扫描。
|
||||
* 缺外部依赖的组会跳过,可选择其后的已就绪组;同轮后面补齐依赖也不回头重扫,留给下一次调用。
|
||||
* 不修改 committed/pending,不读取源或实际提交;输出数组可变且 row 沿用输入引用。
|
||||
* limits 与 locator 成本须先校验;本函数不核对真实编码字节/changes,也没有中途取消或异步让出。 */
|
||||
export function selectImportGroups(pending: readonly ImportLocator[], committed: ReadonlySet<string>, limits: {units: number; bytes: number; changes: number}): readonly (readonly ImportLocator[])[] {
|
||||
// 正向边仅含当前pending内依赖,反向表用于第二次DFS;原index保留稳定源顺序。
|
||||
// byKey 用 key→原记录/源索引,edges 是依赖邻接表,reverse 是依赖→引用者表。
|
||||
// 建图只含 pending 内边;外部 key 暂不入图,稍后用 committed 判定是否满足。
|
||||
// 全段建图与排序同步完成,runner 应先限制 pending 大小,不能靠返回组数限制本次计算成本。
|
||||
const byKey = new Map(pending.map((row, index) => [row.key, {row, index}])), edges = new Map<string, string[]>(), reverse = new Map<string, string[]>();
|
||||
for (const row of pending) {
|
||||
const dependencies = row.dependencies.filter(key => byKey.has(key)); edges.set(row.key, dependencies);
|
||||
@@ -39,11 +44,13 @@ export function selectImportGroups(pending: readonly ImportLocator[], committed:
|
||||
for (const group of components) {
|
||||
const internal = new Set(group.map(row => row.key));
|
||||
if (!group.every(row => row.dependencies.every(key => available.has(key) || internal.has(key)))) continue;
|
||||
// SCC不可拆:单组字节/changes超安全上限必须报错,不能分批提交半个循环引用。
|
||||
// 只有依赖就绪的组才检查硬成本;超限直接抛错,即使前面已选了组也不会返回部分结果。
|
||||
// SCC 不可拆,依赖未就绪的超限组仍先等待,不能用拆组绕过循环引用完整性。
|
||||
const size = group.reduce((sum, row) => sum + row.byteLength, 0), count = group.reduce((sum, row) => sum + row.changeCount, 0);
|
||||
if (size > limits.bytes || count > limits.changes) throw fault('import_dependency_group_capacity');
|
||||
// The frame's adaptive unit budget is soft for one SCC, byte/change safety limits are hard.
|
||||
// 第一组可超过units软预算以闭合环;后续组和字节/changes总预算仍保守限制。
|
||||
// 第一组可超过 units 软预算以闭合环,字节/changes 总量始终不得超限。
|
||||
// 后续组放不下就继续找后面的较小可用组,不以 break 阻止其它独立组;available 只加入实际 chosen 的键。
|
||||
if (units && units + group.length > limits.units || bytes + size > limits.bytes || changes + count > limits.changes) continue;
|
||||
chosen.push(group); units += group.length; bytes += size; changes += count; for (const row of group) available.add(row.key);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user