mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 23:02:27 +08:00
- font-meta 首次计算复用 withMemoryGate 并发闸门(与子集化共享内存预算) - FontMeta 加 metaVersion 版本指纹,算法变更后 .meta.json 自动失效 - 删除 withMemoryGate/initMemoryGate 废弃的 _softLimitMB 参数 - 清理旧 .meta.json 缓存文件
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
/**
|
||
* 字体子集化并发队列(带字体分组调度)
|
||
*
|
||
* 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,
|
||
};
|
||
}
|