feat: font-meta 并发限制 + metaVersion 磁盘缓存失效 + 清理废弃参数

- font-meta 首次计算复用 withMemoryGate 并发闸门(与子集化共享内存预算)
- FontMeta 加 metaVersion 版本指纹,算法变更后 .meta.json 自动失效
- 删除 withMemoryGate/initMemoryGate 废弃的 _softLimitMB 参数
- 清理旧 .meta.json 缓存文件
This commit is contained in:
崮生(子虚) 2026-07-31 00:10:25 +08:00
parent 98c3c5a6e2
commit fa3fa15bf7
5 changed files with 45 additions and 15 deletions

View File

@ -210,7 +210,7 @@ async function main() {
console.log("[config] admin upload:", !!adminApiKey);
/** 初始化子集化并发队列(含字体分组调度) */
initMemoryGate(0, subsetConcurrency);
initMemoryGate(subsetConcurrency);
/** 启动临时字体定时清理器 */
startTempCleaner();

View File

@ -460,6 +460,14 @@ export interface FontMeta {
config?: FontUserConfig;
}
/**
*
*
* extractCodePoints / calcCoverage bump
* .meta.json metaVersion
*/
export const META_VERSION = 3;
/**
* codepoint
* config font-config.json
@ -467,6 +475,7 @@ export interface FontMeta {
export function extractFontMeta(fontBuffer: ArrayBuffer | Uint8Array): FontMeta {
const cps = extractCodePoints(fontBuffer);
return {
metaVersion: META_VERSION,
totalCodePoints: cps.size,
coverage: calcCoverage(cps),
ranges: codePointsToRanges(cps),

View File

@ -1,6 +1,8 @@
import { extractFontMeta, type FontUserConfig } from "../font_util/font_meta.js";
import { extractFontMeta, META_VERSION, type FontUserConfig } from "../font_util/font_meta.js";
import { parseUrl, jsonResponse, findFontPath, readFontBuffer } from "../shared";
import { readFile, writeFile, stat } from "../interface";
import { withMemoryGate } from "../subset_queue";
import { subsetQueueTimeoutSeconds } from "../config";
/**
*
@ -99,17 +101,41 @@ export async function handleFontMeta(req: Request, _res: Response) {
/** 1. 进程内存命中 */
let meta = metaCache.get(fontPath);
if (!meta) {
/** 2. 磁盘 .meta.json 命中 */
/** 2. 磁盘 .meta.json 命中(版本指纹不匹配则视为 miss重新计算 */
meta = await loadMetaFromDisk(fontPath);
if (meta) {
if (meta && meta.metaVersion === META_VERSION) {
metaCache.set(fontPath, meta);
} else {
meta = undefined;
}
}
/** 3. 首次请求 —— 解析 cmap + name 表计算元数据 */
/** 3. 首次请求 —— 解析 cmap + name 表计算元数据CPU/内存密集,复用子集化并发闸门) */
if (!meta) {
const fontBuffer = await readFontBuffer(fontPath);
meta = extractFontMeta(fontBuffer);
/** 排队超时,返回 503 让客户端重试 */
const result = await withMemoryGate(
async () => {
const fontBuffer = await readFontBuffer(fontPath);
return extractFontMeta(fontBuffer);
},
subsetQueueTimeoutSeconds * 1000,
fontPath,
);
if (result === null) {
return {
req,
res: new Response("Server busy, please retry", {
status: 503,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Retry-After": "1",
},
}),
};
}
meta = result;
metaCache.set(fontPath, meta);
/** 异步写盘,不阻塞响应 */
saveMetaToDisk(fontPath, meta);

View File

@ -116,7 +116,7 @@ export async function handleFontSubset(req: Request, res: Response) {
* RSS softLimit
* + GC RSS OOM
*/
const subsetResult = await withMemoryGate(subsetConcurrency, async () => {
const subsetResult = await withMemoryGate(async () => {
return fontSubset(oldFontBuffer, text, {
outType: outType,
sourceType: fontType,

View File

@ -37,16 +37,12 @@ let lastGroupKey = "";
* groupKey
* 使 buffer /
*
* withMemoryGate subset.ts
*
* @param _softLimitMB 使
* @param task
* @param task
* @param queueTimeoutMs
* @param groupKey fontPath
* @returns null
*/
export async function withMemoryGate<T>(
_softLimitMB: number,
task: () => Promise<T>,
queueTimeoutMs: number,
groupKey = "",
@ -121,10 +117,9 @@ function release(): void {
* config
*
* withMemoryGate
* @param _softLimitMB
* @param concurrency
*/
export function initMemoryGate(_softLimitMB: number, concurrency?: number): void {
export function initMemoryGate(concurrency?: number): void {
if (concurrency && concurrency > 0) {
maxConcurrency = concurrency;
}