docs(canvas): 完善画布模块与函数的逻辑注释 #12

Merged
Zayden-Jung merged 42 commits from codex/runtime-replay-comments into main 2026-11-12 07:50:40 +00:00
4 changed files with 31 additions and 11 deletions
Showing only changes of commit d02cd2b01d - Show all commits
+5 -2
View File
@@ -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{
+6 -2
View File
@@ -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;}
+8 -2
View File
@@ -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;
+12 -5
View File
@@ -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);
}