mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 14:53:32 +08:00
feat: 排队请求按字体分组,同字体连续处理
- withMemoryGate 新增 groupKey 参数,subset.ts 传入 fontPath - pickNext: 优先唤醒与上一个相同 groupKey 的等待者 - 同字体请求连续处理:字体 buffer/解析对象在 GC 窗口内被复用 - 大幅降低排队积压时的内存峰值(避免反复加载不同字体)
This commit is contained in:
parent
7403414439
commit
d2f6717348
@ -121,7 +121,7 @@ export async function handleFontSubset(req: Request, res: Response) {
|
||||
outType: outType,
|
||||
sourceType: fontType,
|
||||
});
|
||||
}, subsetQueueTimeoutSeconds * 1000);
|
||||
}, subsetQueueTimeoutSeconds * 1000, fontPath);
|
||||
|
||||
/** 排队超时,返回 503 让客户端重试 */
|
||||
if (subsetResult === null) {
|
||||
|
||||
@ -57,12 +57,23 @@ function gc(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** 等待队列(FIFO):softLimit 内的请求完成后依次唤醒 */
|
||||
const waitQueue: Array<() => void> = [];
|
||||
/** 等待队列项:resolver + groupKey(用于同字体分组连续唤醒) */
|
||||
interface QueueItem {
|
||||
/** 唤醒函数 */
|
||||
resolve: () => void;
|
||||
/** 分组键(通常为 fontPath),同组优先连续处理以复用字体缓存 */
|
||||
groupKey: string;
|
||||
}
|
||||
|
||||
/** 等待队列:checkAndNotify 按分组优先级唤醒 */
|
||||
const waitQueue: QueueItem[] = [];
|
||||
|
||||
/** 当前正在执行的子集化数量(用于 stats 展示) */
|
||||
let activeCount = 0;
|
||||
|
||||
/** 最近一次执行的 groupKey——同组请求优先连续唤醒 */
|
||||
let lastGroupKey = "";
|
||||
|
||||
/** softLimit 引用(checkAndNotify 需要用) */
|
||||
let softLimitRef = 0;
|
||||
|
||||
@ -73,23 +84,29 @@ let softLimitRef = 0;
|
||||
* - RSS 超 softLimit → 排队等待,前面的请求完成后 GC → RSS 回落 → 唤醒
|
||||
* - 队列超时 → 返回 null(调用方返回 503,客户端可重试)
|
||||
*
|
||||
* 分组优化:同 groupKey(同一字体)的排队请求优先连续唤醒,
|
||||
* 使字体 buffer / 解析对象在 GC 窗口内被下一个请求复用,降低内存峰值。
|
||||
*
|
||||
* @param softLimitMB 内存软限制(MB),RSS 超过此值时新请求排队
|
||||
* @param task 实际的子集化异步任务
|
||||
* @param queueTimeoutMs 排队超时(毫秒)
|
||||
* @param groupKey 分组键(通常为 fontPath),同组连续处理以复用缓存
|
||||
* @returns 任务结果,或 null 表示排队超时
|
||||
*/
|
||||
export async function withMemoryGate<T>(
|
||||
softLimitMB: number,
|
||||
task: () => Promise<T>,
|
||||
queueTimeoutMs: number,
|
||||
groupKey = "",
|
||||
): Promise<T | null> {
|
||||
/** RSS=0 表示无法读取(开发环境),跳过闸门直接执行 */
|
||||
if (softLimitMB > 0 && getRssMB() >= softLimitMB) {
|
||||
/** 内存超阈值,进入排队 */
|
||||
const acquired = await waitForSlot(queueTimeoutMs);
|
||||
const acquired = await waitForSlot(queueTimeoutMs, groupKey);
|
||||
if (!acquired) return null;
|
||||
}
|
||||
|
||||
lastGroupKey = groupKey;
|
||||
activeCount++;
|
||||
try {
|
||||
return await task();
|
||||
@ -97,7 +114,7 @@ export async function withMemoryGate<T>(
|
||||
activeCount--;
|
||||
/**
|
||||
* 任务完成后主动 GC,加速释放字体解析的大对象。
|
||||
* 然后检查队列:如果 RSS 已回落,唤醒下一个等待者。
|
||||
* 然后检查队列:如果 RSS 已回落,唤醒下一个等待者(同 groupKey 优先)。
|
||||
*/
|
||||
gc();
|
||||
checkAndNotify();
|
||||
@ -107,30 +124,49 @@ export async function withMemoryGate<T>(
|
||||
/**
|
||||
* 排队等待内存释放
|
||||
*
|
||||
* 加入 FIFO 队列,等待前面的请求完成后唤醒。
|
||||
* 加入队列,等待前面的请求完成后唤醒(同 groupKey 优先)。
|
||||
* 超时则从队列移除自己,返回 false。
|
||||
*/
|
||||
function waitForSlot(timeoutMs: number): Promise<boolean> {
|
||||
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(resolver);
|
||||
const idx = waitQueue.indexOf(item);
|
||||
if (idx !== -1) waitQueue.splice(idx, 1);
|
||||
resolve(false);
|
||||
}, timeoutMs);
|
||||
|
||||
const resolver = () => {
|
||||
/** 覆盖 resolve 以便唤醒时清 timer */
|
||||
item.resolve = () => {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
};
|
||||
waitQueue.push(resolver);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查内存并唤醒队列
|
||||
*
|
||||
* 每个子集化任务完成后调用:
|
||||
* 1. 如果 RSS < softLimit 且队列非空 → 唤醒下一个
|
||||
* 1. 如果 RSS < softLimit 且队列非空 → 唤醒下一个(同 groupKey 优先)
|
||||
* 2. RSS 仍超限 → 不唤醒(等待引擎自动 GC 后下次再检查)
|
||||
* 3. 兜底:5 秒后如果 RSS 没超 softLimit×1.2,强制唤醒(防饿死)
|
||||
*/
|
||||
@ -138,12 +174,12 @@ function checkAndNotify(): void {
|
||||
if (waitQueue.length === 0) return;
|
||||
/** RSS 未知(开发环境)→ 直接唤醒 */
|
||||
if (softLimitRef === 0) {
|
||||
waitQueue.shift()?.();
|
||||
pickNext()?.resolve();
|
||||
return;
|
||||
}
|
||||
/** RSS 已回落到阈值以下 → 唤醒下一个 */
|
||||
if (getRssMB() < softLimitRef) {
|
||||
waitQueue.shift()?.();
|
||||
pickNext()?.resolve();
|
||||
/** 唤醒后递归检查:可能还有内存余量给更多等待者 */
|
||||
checkAndNotify();
|
||||
return;
|
||||
@ -154,7 +190,7 @@ function checkAndNotify(): void {
|
||||
*/
|
||||
setTimeout(() => {
|
||||
if (waitQueue.length > 0 && getRssMB() < softLimitRef * 1.2) {
|
||||
waitQueue.shift()?.();
|
||||
pickNext()?.resolve();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user