web-font/backend/subset_queue.ts
崮生(子虚) 65ba984972 feat: 字体子集化并发队列控制(服务端+客户端)
后端:
- subset_queue.ts: Semaphore 并发控制器,排队超时返回 503
- subset.ts: fontSubset 调用包裹在 withConcurrencyLimit 中
- config.ts: 新增 SUBSET_CONCURRENCY 环境变量(默认 2)
- routes/config.ts: 暴露 subsetConcurrency 到配置 API

前端:
- api.ts: ServerConfig 新增 subsetConcurrency 字段
- webfont-sdk.js: 客户端并发池(默认 4),setMaxConcurrent API
  通过 document.fonts.load 精确追踪字体加载完成释放槽位
2026-07-30 21:33:45 +08:00

115 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 字体子集化并发队列控制器
*
* 字体裁剪是 CPU/内存密集操作(尤其大字集 woff2 brotli 压缩),
* LLRT 运行时内存受限(~900M并发过多会导致 brotli decoder OOM 崩溃。
*
* 通过 Semaphore 限制同时执行的子集化数量,超出并发的请求排队等待,
* 等待超时则返回 503避免请求无限堆积。
*/
/** 等待队列中的请求超时时间(毫秒),默认 10 秒 */
const QUEUE_TIMEOUT_MS = 10_000;
/** 当前正在执行的子集化数量 */
let activeCount = 0;
/** 当前排队等待的数量 */
let waitingCount = 0;
/**
* 通过 Semaphore 执行子集化任务
*
* - 并发未满时立即执行
* - 并发已满时排队等待,超时返回 null调用方应返回 503
* - 返回任务结果,或 null 表示排队超时
*
* @param maxConcurrency 最大并发数
* @param task 实际的子集化异步任务
* @returns 任务结果,或 null 表示排队超时
*/
export async function withConcurrencyLimit<T>(
maxConcurrency: number,
task: () => Promise<T>,
): Promise<T | null> {
/** 并发未满,直接执行 */
if (activeCount < maxConcurrency) {
activeCount++;
try {
return await task();
} finally {
activeCount--;
/** 唤醒一个等待者(如果有)—— 通过 resolve 触发 */
notifyNext();
}
}
/** 并发已满,进入排队 */
waitingCount++;
try {
/** 等待获取许可,或超时 */
const acquired = await waitForSlot(QUEUE_TIMEOUT_MS);
if (!acquired) {
/** 排队超时,返回 null 让调用方返回 503 */
return null;
}
activeCount++;
try {
return await task();
} finally {
activeCount--;
notifyNext();
}
} finally {
waitingCount--;
}
}
/** 等待队列FIFO */
const waitQueue: Array<() => void> = [];
/**
* 等待获取一个并发许可
*
* @param timeout 超时时间(毫秒)
* @returns true=获得许可false=超时
*/
function waitForSlot(timeout: number): Promise<boolean> {
return new Promise((resolve) => {
/** 超时定时器 */
const timer = setTimeout(() => {
/** 从队列中移除自己 */
const idx = waitQueue.indexOf(resolver);
if (idx !== -1) waitQueue.splice(idx, 1);
resolve(false);
}, timeout);
/** resolve 包装:清除定时器再 resolve */
const resolver = () => {
clearTimeout(timer);
resolve(true);
};
waitQueue.push(resolver);
});
}
/**
* 唤醒队列中下一个等待者
*
* 在 activeCount 减少后调用,让排队中的请求依次进入。
*/
function notifyNext() {
/** 仍有空位且有人在等 */
const resolver = waitQueue.shift();
if (resolver) {
resolver();
}
}
/** 获取当前队列状态(用于 stats / 日志) */
export function getQueueStats() {
return {
active: activeCount,
waiting: waitingCount,
};
}