From d2f671734858cb8a540b86aab737f462a3ace762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Thu, 30 Jul 2026 22:53:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=92=E9=98=9F=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=8C=89=E5=AD=97=E4=BD=93=E5=88=86=E7=BB=84=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E8=BF=9E=E7=BB=AD=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - withMemoryGate 新增 groupKey 参数,subset.ts 传入 fontPath - pickNext: 优先唤醒与上一个相同 groupKey 的等待者 - 同字体请求连续处理:字体 buffer/解析对象在 GC 窗口内被复用 - 大幅降低排队积压时的内存峰值(避免反复加载不同字体) --- backend/routes/subset.ts | 2 +- backend/subset_queue.ts | 64 +++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/backend/routes/subset.ts b/backend/routes/subset.ts index 60cae14..013ddce 100644 --- a/backend/routes/subset.ts +++ b/backend/routes/subset.ts @@ -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) { diff --git a/backend/subset_queue.ts b/backend/subset_queue.ts index bfa7482..175921b 100644 --- a/backend/subset_queue.ts +++ b/backend/subset_queue.ts @@ -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( softLimitMB: number, task: () => Promise, queueTimeoutMs: number, + groupKey = "", ): Promise { /** 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( activeCount--; /** * 任务完成后主动 GC,加速释放字体解析的大对象。 - * 然后检查队列:如果 RSS 已回落,唤醒下一个等待者。 + * 然后检查队列:如果 RSS 已回落,唤醒下一个等待者(同 groupKey 优先)。 */ gc(); checkAndNotify(); @@ -107,30 +124,49 @@ export async function withMemoryGate( /** * 排队等待内存释放 * - * 加入 FIFO 队列,等待前面的请求完成后唤醒。 + * 加入队列,等待前面的请求完成后唤醒(同 groupKey 优先)。 * 超时则从队列移除自己,返回 false。 */ -function waitForSlot(timeoutMs: number): Promise { +function waitForSlot(timeoutMs: number, groupKey: string): Promise { 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); }