web-font/backend/subset_queue.ts
崮生(子虚) 7403414439 feat: 用内存水位闸门替代固定并发限制
- subset_queue: 新增 withMemoryGate,基于 RSS 决定排队/执行
- getRssMB: Node 用 process.memoryUsage(),LLRT 用 /proc/self/statm
- 请求完成后主动 __gc() 加速内存回收
- config: SUBSET_MEM_SOFT_LIMIT_MB=600 替代 SUBSET_CONCURRENCY=4
- 小请求可高并发(内存够),大请求自动排队(防 OOM)
2026-07-30 22:51:37 +08:00

184 lines
5.7 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字体裁剪是内存密集操作。
* 不用固定并发数,而是实时监控进程 RSS
* - RSS < softLimit直接执行小请求可高并发
* - RSS ≥ softLimit排队等待直到前面的请求完成 + GC 释放内存
* - 无硬限制/拒绝:所有请求最终都会执行
*
* 内存监控通过 /proc/self/statmLinux 唯一可用方式LLRT 无 process.memoryUsage
* GC 通过 LLRT 内置的 __gc() 主动触发(请求完成后调用,加速内存回收)。
*/
/** /proc/self/statm 文件描述符(启动时打开,反复读取不需每次 open/close */
let statmFd: number | null = null;
/**
* 读取当前进程 RSS常驻内存单位 MB
*
* Node 环境使用 process.memoryUsage().rssLLRT 无此 API改用 /proc/self/statm。
* /proc/self/statm 格式size resident shared text lib data dt单位
* resident 字段 × 页大小(4096) = RSS 字节数。
*/
function getRssMB(): number {
try {
/** Node 环境优先使用 process.memoryUsage() */
if (typeof process !== "undefined" && process.memoryUsage) {
return Math.round(process.memoryUsage().rss / 1024 / 1024);
}
/** LLRT 环境:读取 /proc/self/statm */
if (statmFd === null) {
const { openSync } = require("fs");
statmFd = openSync("/proc/self/statm", "r");
}
const { readSync } = require("fs");
const buf = new Uint8Array(256);
const n = readSync(statmFd, buf, 0, 256, 0);
const parts = new TextDecoder().decode(buf.subarray(0, n)).trim().split(" ");
return Math.round((parseInt(parts[1]) * 4096) / 1024 / 1024);
} catch {
/** 两个方案都不可用,返回 0 表示「无限制」 */
return 0;
}
}
/**
* 主动触发垃圾回收
*
* LLRT 内置 __gc(),请求完成后调用可立即释放字体解析产生的大对象,
* 而非等待 LLRT 引擎自动 GC可能延迟数秒
*/
function gc(): void {
try {
(globalThis as any).__gc?.();
} catch {
/** __gc 不存在Node 环境)时静默跳过 */
}
}
/** 等待队列FIFOsoftLimit 内的请求完成后依次唤醒 */
const waitQueue: Array<() => void> = [];
/** 当前正在执行的子集化数量(用于 stats 展示) */
let activeCount = 0;
/** softLimit 引用checkAndNotify 需要用) */
let softLimitRef = 0;
/**
* 通过内存水位闸门执行子集化任务
*
* - RSS 未超 softLimit → 立即执行
* - RSS 超 softLimit → 排队等待,前面的请求完成后 GC → RSS 回落 → 唤醒
* - 队列超时 → 返回 null调用方返回 503客户端可重试
*
* @param softLimitMB 内存软限制MBRSS 超过此值时新请求排队
* @param task 实际的子集化异步任务
* @param queueTimeoutMs 排队超时(毫秒)
* @returns 任务结果,或 null 表示排队超时
*/
export async function withMemoryGate<T>(
softLimitMB: number,
task: () => Promise<T>,
queueTimeoutMs: number,
): Promise<T | null> {
/** RSS=0 表示无法读取(开发环境),跳过闸门直接执行 */
if (softLimitMB > 0 && getRssMB() >= softLimitMB) {
/** 内存超阈值,进入排队 */
const acquired = await waitForSlot(queueTimeoutMs);
if (!acquired) return null;
}
activeCount++;
try {
return await task();
} finally {
activeCount--;
/**
* 任务完成后主动 GC加速释放字体解析的大对象。
* 然后检查队列:如果 RSS 已回落,唤醒下一个等待者。
*/
gc();
checkAndNotify();
}
}
/**
* 排队等待内存释放
*
* 加入 FIFO 队列,等待前面的请求完成后唤醒。
* 超时则从队列移除自己,返回 false。
*/
function waitForSlot(timeoutMs: number): Promise<boolean> {
return new Promise((resolve) => {
const timer = setTimeout(() => {
const idx = waitQueue.indexOf(resolver);
if (idx !== -1) waitQueue.splice(idx, 1);
resolve(false);
}, timeoutMs);
const resolver = () => {
clearTimeout(timer);
resolve(true);
};
waitQueue.push(resolver);
});
}
/**
* 检查内存并唤醒队列
*
* 每个子集化任务完成后调用:
* 1. 如果 RSS < softLimit 且队列非空 → 唤醒下一个
* 2. RSS 仍超限 → 不唤醒(等待引擎自动 GC 后下次再检查)
* 3. 兜底5 秒后如果 RSS 没超 softLimit×1.2,强制唤醒(防饿死)
*/
function checkAndNotify(): void {
if (waitQueue.length === 0) return;
/** RSS 未知(开发环境)→ 直接唤醒 */
if (softLimitRef === 0) {
waitQueue.shift()?.();
return;
}
/** RSS 已回落到阈值以下 → 唤醒下一个 */
if (getRssMB() < softLimitRef) {
waitQueue.shift()?.();
/** 唤醒后递归检查:可能还有内存余量给更多等待者 */
checkAndNotify();
return;
}
/**
* RSS 仍超限时不立即唤醒——设置兜底定时器:
* 5 秒后如果 RSS 降到 softLimit×1.2 以内,唤醒一个(防极端饿死)。
*/
setTimeout(() => {
if (waitQueue.length > 0 && getRssMB() < softLimitRef * 1.2) {
waitQueue.shift()?.();
}
}, 5000);
}
/**
* 初始化闸门参数(由 config.ts 的值设置)
*
* 必须在第一次 withMemoryGate 调用前执行。
* @param softLimitMB 内存软限制MB
*/
export function initMemoryGate(softLimitMB: number): void {
softLimitRef = softLimitMB;
const rss = getRssMB();
if (rss > 0) {
console.log(`[memgate] RSS=${rss}MB, softLimit=${softLimitMB}MB`);
}
}
/** 获取当前队列状态(用于 stats / 日志) */
export function getQueueStats() {
return {
active: activeCount,
waiting: waitQueue.length,
rssMB: getRssMB(),
};
}