web-font/backend/subset_queue.ts
崮生(子虚) fa3fa15bf7 feat: font-meta 并发限制 + metaVersion 磁盘缓存失效 + 清理废弃参数
- font-meta 首次计算复用 withMemoryGate 并发闸门(与子集化共享内存预算)
- FontMeta 加 metaVersion 版本指纹,算法变更后 .meta.json 自动失效
- 删除 withMemoryGate/initMemoryGate 废弃的 _softLimitMB 参数
- 清理旧 .meta.json 缓存文件
2026-07-31 00:10:25 +08:00

136 lines
4.0 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.

/**
* 字体子集化并发队列(带字体分组调度)
*
* LLRT 运行时内存受限(~900M字体裁剪是内存密集操作。
* 通过固定并发数限制同时执行的子集化任务,防止 OOM 崩溃。
*
* 分组优化排队中的请求按字体groupKey分组同字体的请求优先连续处理
* 使字体 buffer / 解析对象在缓存窗口内被下一个请求复用,降低内存峰值。
*/
/** 等待队列项resolver + groupKey用于同字体分组连续唤醒 */
interface QueueItem {
resolve: () => void;
/** 分组键(通常为 fontPath同组优先连续处理以复用字体缓存 */
groupKey: string;
}
/** 等待队列release 按分组优先级唤醒 */
const waitQueue: QueueItem[] = [];
/** 当前正在执行的子集化数量 */
let activeCount = 0;
/** 最大并发数(由 initConcurrency 设置) */
let maxConcurrency = 4;
/** 最近一次执行的 groupKey——同组请求优先连续唤醒 */
let lastGroupKey = "";
/**
* 通过并发队列执行子集化任务
*
* - active < maxConcurrency → 立即执行
* - active ≥ maxConcurrency → 排队等待,前面的完成后唤醒(同 groupKey 优先)
* - 队列超时 → 返回 null调用方返回 503客户端可重试
*
* 分组优化:同 groupKey同一字体的排队请求优先连续唤醒
* 使字体 buffer / 解析对象在缓存窗口内被下一个请求复用,降低内存峰值。
*
* @param task 实际的异步任务
* @param queueTimeoutMs 排队超时(毫秒)
* @param groupKey 分组键(通常为 fontPath同组连续处理以复用缓存
* @returns 任务结果,或 null 表示排队超时
*/
export async function withMemoryGate<T>(
task: () => Promise<T>,
queueTimeoutMs: number,
groupKey = "",
): Promise<T | null> {
/** 并发已满,进入排队 */
if (activeCount >= maxConcurrency) {
const acquired = await waitForSlot(queueTimeoutMs, groupKey);
if (!acquired) return null;
}
lastGroupKey = groupKey;
activeCount++;
try {
return await task();
} finally {
activeCount--;
release();
}
}
/**
* 排队等待并发槽位
*
* 加入队列,等待前面的请求完成后唤醒(同 groupKey 优先)。
* 超时则从队列移除自己,返回 false。
*/
function waitForSlot(timeoutMs: number, groupKey: string): Promise<boolean> {
return new Promise((resolve) => {
const item: QueueItem = {
resolve: () => resolve(true),
groupKey,
};
const timer = setTimeout(() => {
const idx = waitQueue.indexOf(item);
if (idx !== -1) waitQueue.splice(idx, 1);
resolve(false);
}, timeoutMs);
item.resolve = () => {
clearTimeout(timer);
resolve(true);
};
waitQueue.push(item);
});
}
/**
* 从队列中取出下一个该唤醒的项
*
* 策略:优先选与 lastGroupKey 相同的项(同字体连续处理,复用缓存);
* 没有同组项则取队首FIFO 公平)。
*/
function pickNext(): QueueItem | undefined {
if (waitQueue.length === 0) return undefined;
if (lastGroupKey) {
const idx = waitQueue.findIndex((item) => item.groupKey === lastGroupKey);
if (idx !== -1) return waitQueue.splice(idx, 1)[0];
}
return waitQueue.shift();
}
/**
* 释放一个并发槽位,唤醒下一个等待者(同 groupKey 优先)
*/
function release(): void {
if (waitQueue.length === 0) return;
if (activeCount < maxConcurrency) {
pickNext()?.resolve();
}
}
/**
* 初始化并发参数(由 config 设置)
*
* 必须在第一次 withMemoryGate 调用前执行。
* @param concurrency 最大并发数
*/
export function initMemoryGate(concurrency?: number): void {
if (concurrency && concurrency > 0) {
maxConcurrency = concurrency;
}
console.log(`[subset-queue] maxConcurrency=${maxConcurrency}`);
}
/** 获取当前队列状态(用于 stats / 日志) */
export function getQueueStats() {
return {
active: activeCount,
waiting: waitQueue.length,
};
}