feat: 排队请求按字体分组,同字体连续处理

- withMemoryGate 新增 groupKey 参数,subset.ts 传入 fontPath
- pickNext: 优先唤醒与上一个相同 groupKey 的等待者
- 同字体请求连续处理:字体 buffer/解析对象在 GC 窗口内被复用
- 大幅降低排队积压时的内存峰值(避免反复加载不同字体)
This commit is contained in:
崮生(子虚) 2026-07-30 22:53:39 +08:00
parent 7403414439
commit d2f6717348
2 changed files with 51 additions and 15 deletions

View File

@ -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) {

View File

@ -57,12 +57,23 @@ function gc(): void {
}
}
/** 等待队列FIFOsoftLimit 内的请求完成后依次唤醒 */
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 MBRSS
* @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);
}