perf(gsub): subsetGSUB 缓存+数组查找+裸读,FiraCode 983ms→30ms

FiraCode-代码用例(403 lookup)子集化从 983ms 降至 ~30ms,SSIM 全程保持 0.9923。

核心优化(CPU profile 逐层下钻):
- coverage 解析+重映射缓存(off→{gids,remapped,outOfSubset}):format3 的 coverage
  被多 subtable 引用,解析与重映射结果按 off 复用;readCoverageRemapped 边解析边过滤,
  format3 不再分配中间原 gid 数组
- OTReader.dv 公开:coverage 偏移来自已验证 subtable,热循环用裸 DataView.getUint16
  绕过 u16 的逐次边界检查
- GidLookup Int32Array(下标=原gid,值=新gid,-1=不在子集)替代 Map.get,coverage 过滤
  的高频查询从哈希退化为数组索引
- OTWriter.rollback 改 pop:patches 按 pos 单调追加,回退从末尾 pop 替代全量 filter
  (FiraCode 每次 rollback 392 次)
- gsub-reachable coverage 缓存:不动点迭代每轮重复解析同一 coverage,按 off 缓存

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-23 13:33:19 +08:00
parent a84a4aa6f2
commit 28b6b1ebf4
3 changed files with 187 additions and 36 deletions

View File

@ -28,8 +28,17 @@ const LT_REVERSE_CHAIN = 5;
const LT_CHAIN = 6;
const LT_EXTENSION = 7;
/** 读取 Coverage 表的 gid 列表 */
function readCoverageGids(r: OTReader, off: number): number[] {
/**
* Coverage off gid
* lookup coverage coverage
* FiraCode u16 reachable
*/
type CoverageCache = Map<number, number[]>;
/** 读取 Coverage 表的 gid 列表。传入 cache 时按绝对偏移缓存解析结果。 */
function readCoverageGids(r: OTReader, off: number, cache: CoverageCache): number[] {
const hit = cache.get(off);
if (hit !== undefined) return hit;
const format = r.u16(off);
const gids: number[] = [];
if (format === 1) {
@ -45,6 +54,7 @@ function readCoverageGids(r: OTReader, off: number): number[] {
p += 6;
}
}
cache.set(off, gids);
return gids;
}
@ -61,6 +71,7 @@ export function collectReachableGsubTargets(
): Set<number> {
const dv = new DataView(gsubBytes.buffer, gsubBytes.byteOffset, gsubBytes.byteLength);
const r = new OTReader(dv);
const covCache: CoverageCache = new Map();
const major = r.u16(0);
const minor = r.u16(2);
@ -120,7 +131,7 @@ export function collectReachableGsubTargets(
/** type6收集可触发规则引用的 lookup index 与所需 context gid */
const refs = new Set<number>();
const ctxGids = new Set<number>();
collectChainRefs(r, subAbs, refs, ctxGids, inSubset);
collectChainRefs(r, subAbs, refs, ctxGids, inSubset, covCache);
for (const g of ctxGids) {
if (!reachable.has(g)) { reachable.add(g); changed = true; }
}
@ -128,14 +139,14 @@ export function collectReachableGsubTargets(
const refLk = lookups[li];
if (!refLk) continue;
for (const refSub of refLk.subtableAbsOffs) {
const refTargets = collectSubtableTargets(r, refSub, refLk.effectiveType, inSubset);
const refTargets = collectSubtableTargets(r, refSub, refLk.effectiveType, inSubset, covCache);
for (const g of refTargets) {
if (!reachable.has(g)) { reachable.add(g); changed = true; }
}
}
}
} else {
const newTargets = collectSubtableTargets(r, subAbs, lk.effectiveType, inSubset);
const newTargets = collectSubtableTargets(r, subAbs, lk.effectiveType, inSubset, covCache);
for (const g of newTargets) {
if (!reachable.has(g)) {
reachable.add(g);
@ -157,13 +168,14 @@ function collectSubtableTargets(
off: number,
type: number,
inSubset: (gid: number) => boolean,
covCache: CoverageCache,
): number[] {
const targets: number[] = [];
if (type === LT_SINGLE) {
/** SingleSubst: format1 coverage+delta / format2 coverage+gidArray */
const format = r.u16(off);
const covOff = off + r.u16(off + 2);
const covGids = readCoverageGids(r, covOff);
const covGids = readCoverageGids(r, covOff, covCache);
if (format === 1) {
const delta = r.i16(off + 4);
for (const g of covGids) {
@ -178,7 +190,7 @@ function collectSubtableTargets(
} else if (type === LT_MULTIPLE) {
const covOff = off + r.u16(off + 2);
const seqCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff);
const covGids = readCoverageGids(r, covOff, covCache);
for (let i = 0; i < covGids.length && i < seqCount; i++) {
if (!inSubset(covGids[i])) continue;
const seqOff = off + r.u16(off + 6 + i * 2);
@ -188,7 +200,7 @@ function collectSubtableTargets(
} else if (type === LT_ALTERNATE) {
const covOff = off + r.u16(off + 2);
const altCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff);
const covGids = readCoverageGids(r, covOff, covCache);
for (let i = 0; i < covGids.length && i < altCount; i++) {
if (!inSubset(covGids[i])) continue;
const altOff = off + r.u16(off + 6 + i * 2);
@ -199,7 +211,7 @@ function collectSubtableTargets(
/** LigatureSubst: 全部 component 在子集 → target gid */
const covOff = off + r.u16(off + 2);
const setCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff);
const covGids = readCoverageGids(r, covOff, covCache);
for (let i = 0; i < covGids.length && i < setCount; i++) {
const firstInSubset = inSubset(covGids[i]);
const setOff = off + r.u16(off + 6 + i * 2);
@ -244,12 +256,13 @@ function collectChainRefs(
refs: Set<number>,
contextGids: Set<number>,
inSubset: (gid: number) => boolean,
covCache: CoverageCache,
): void {
const format = r.u16(off);
if (format === 1) {
/** format1: coverage(gid) + SubRuleSet 数组,按 coverage gid 索引 */
const covOff = off + r.u16(off + 2);
const covGids = readCoverageGids(r, covOff);
const covGids = readCoverageGids(r, covOff, covCache);
const setCount = r.u16(off + 4);
for (let i = 0; i < covGids.length && i < setCount; i++) {
/** 第一分量coverage gid须在子集否则该 SubRuleSet 不触发 */
@ -287,7 +300,7 @@ function collectChainRefs(
let triggerable = true;
const readCovGids = (cnt: number): boolean => {
for (let k = 0; k < cnt; k++) {
const covGids = readCoverageGids(r, off + r.u16(p + k * 2));
const covGids = readCoverageGids(r, off + r.u16(p + k * 2), covCache);
for (const g of covGids) {
allGids.push(g);
if (!inSubset(g)) triggerable = false;

View File

@ -47,19 +47,68 @@ function remapGid(origToNew: Map<number, number>, gid: number): number | null {
/** Coverage 表单个 range 展开为 gid 的上限保护:超出视为偏移错位读到垃圾数据。 */
const COVERAGE_MAX_EXPAND = 0x10000;
/** 读取 Coverage 表,返回覆盖的原 gid 列表(保持顺序)。 */
function readCoverageGids(r: Reader, off: number): number[] {
const format = r.u16(off);
const gids: number[] = [];
/**
* Coverage off
* FiraCode calt ChainContextSubst format3 coverage subtable
* 604 / 83 coverage coverage 126
* gid gid ~86% u16
* map/filter subsetGSUB CPU+GC
* subsetGSUB origToNew
*/
interface CoverageCacheEntry {
/** 原 gid 列表type1/2/3/4 按 index 配对用) */
gids: number[];
/** 重映射后新 gid 列表format3 coverage 数组直接用懒计算null 表示尚未计算 */
remapped: number[] | null;
/** 原 coverage 非空但全部 gid 落子集外 → true调用方据此判该 coverage 失效(区别于原本就空的合法 coverage */
outOfSubset: boolean;
}
type CoverageCache = Map<number, CoverageCacheEntry>;
/**
* gid gid
* origToNew Map<number,number> .get() coverage gid
* Int32Array =gid=gid-1 退
* Map.get subsetGSUB readCoverageRemapped gid 65536 256KB
*/
type GidLookup = Int32Array;
/** Coverage gid
* cache coverage off
* coverage subtable dv.getUint16
* u16 + errorFlag subsetGSUB CPU */
function readCoverageGids(r: Reader, off: number, cache?: CoverageCache): number[] {
if (cache) {
const hit = cache.get(off);
if (hit !== undefined) return hit.gids;
}
const dv = r.dv;
const len = dv.byteLength;
/** coverage 偏移合法性兜底:越界则按错误处理(返回空,调用方降级) */
if (off < 0 || off + 4 > len) {
if (cache) cache.set(off, { gids: [], remapped: null, outOfSubset: false });
return [];
}
const format = dv.getUint16(off, false);
let gids: number[] = [];
if (format === COV_LIST) {
const count = r.u16(off + 2);
for (let i = 0; i < count; i++) gids.push(r.u16(off + 4 + i * 2));
const count = dv.getUint16(off + 2, false);
const base = off + 4;
if (base + count * 2 > len) {
if (cache) cache.set(off, { gids: [], remapped: null, outOfSubset: false });
return [];
}
/** 预分配 + 索引赋值,避免 push 动态扩容format1 coverage 的高频热循环) */
gids = new Array(count);
for (let i = 0; i < count; i++) gids[i] = dv.getUint16(base + i * 2, false);
} else if (format === COV_RANGE) {
const rangeCount = r.u16(off + 2);
const rangeCount = dv.getUint16(off + 2, false);
let p = off + 4;
for (let i = 0; i < rangeCount; i++) {
const start = r.u16(p);
const end = r.u16(p + 2);
/** range 记录 6 字节,越界说明偏移错位读到垃圾 rangeCount停止解析返回已收集部分 */
if (p + 6 > len) break;
const start = dv.getUint16(p, false);
const end = dv.getUint16(p + 2, false);
/** end < start range
* Coverage gid glyph < 0x10000
* gid */
@ -70,9 +119,73 @@ function readCoverageGids(r: Reader, off: number): number[] {
p += 6;
}
}
if (cache) cache.set(off, { gids, remapped: null, outOfSubset: false });
return gids;
}
/**
* coverage gid
* format3 coverage index "子集内新 gid 集合"
* gid gid format3 subsetGSUB
* + off coverage subtable
* @returns gid coverage gid null
* coverage
*/
function readCoverageRemapped(
r: Reader,
off: number,
gidLookup: GidLookup,
cache: CoverageCache,
): number[] | null {
let entry = cache.get(off);
if (entry !== undefined) {
/** 已缓存:失效返回 null否则返回重映射数组remapped 已在首次计算时填好) */
return entry.outOfSubset ? null : (entry.remapped as number[]);
}
const dv = r.dv;
const len = dv.byteLength;
/** 失效/空结果占位(首次计算后回填 cache */
let newGids: number[] = [];
let origNonEmpty = false;
let outOfSubset = false;
if (off < 0 || off + 4 > len) {
/** 越界,按空 coverage 处理 */
} else {
const format = dv.getUint16(off, false);
if (format === COV_LIST) {
const count = dv.getUint16(off + 2, false);
const base = off + 4;
if (base + count * 2 <= len) {
origNonEmpty = count > 0;
for (let i = 0; i < count; i++) {
const m = gidLookup[dv.getUint16(base + i * 2, false)];
if (m >= 0) newGids.push(m);
}
}
} else if (format === COV_RANGE) {
const rangeCount = dv.getUint16(off + 2, false);
let p = off + 4;
for (let i = 0; i < rangeCount; i++) {
if (p + 6 > len) break;
const start = dv.getUint16(p, false);
const end = dv.getUint16(p + 2, false);
if (end >= start && end - start < COVERAGE_MAX_EXPAND && newGids.length + (end - start + 1) <= COVERAGE_MAX_EXPAND) {
for (let g = start; g <= end; g++) {
origNonEmpty = true;
const m = gidLookup[g];
if (m >= 0) newGids.push(m);
}
}
p += 6;
}
}
}
/** 原 coverage 非空但全部 gid 落子集外 → 失效(与原 coverage 本就空的合法空数组区分) */
if (newGids.length === 0 && origNonEmpty) outOfSubset = true;
cache.set(off, { gids: newGids, remapped: newGids, outOfSubset });
return outOfSubset ? null : newGids;
}
/**
* gid Coverage Writer
* format1 format2
@ -447,12 +560,14 @@ function serializeChainedContextSubst(
r: Reader,
off: number,
origToNew: Map<number, number>,
covCache: CoverageCache,
gidLookup: GidLookup,
): boolean {
const format = r.u16(off);
if (format === 1) {
/** coverage(gid) + 子规则数组,每规则含 backtrack/input/lookahead gid 序列 + SubstLookupRecord */
const covOff = off + r.u16(off + 2);
const covGids = readCoverageGids(r, covOff);
const covGids = readCoverageGids(r, covOff, covCache);
const ruleSetCount = r.u16(off + 4);
if (ruleSetCount > 0x7fff) return false;
/** 按 coverage 字形收集有效规则 */
@ -529,13 +644,13 @@ function serializeChainedContextSubst(
}
}
if (classToRules.size === 0) return false;
writeChainFormat2(w, r, coverageOff, backtrackCDOff, inputCDOff, lookaheadCDOff, origToNew, classToRules);
writeChainFormat2(w, r, coverageOff, backtrackCDOff, inputCDOff, lookaheadCDOff, origToNew, classToRules, covCache, gidLookup);
return true;
}
if (format === 3) {
/** 显式 coverage 数组 + SubstLookupRecord */
const parsed = parseChainFormat3(r, off, origToNew);
const parsed = parseChainFormat3(r, off, covCache, gidLookup);
if (!parsed) return false;
writeChainFormat3(w, parsed);
return true;
@ -660,6 +775,8 @@ function writeChainFormat2(
lookaheadCDOff: number,
origToNew: Map<number, number>,
classToRules: Map<number, Array<{ back: number[]; input: number[]; look: number[]; records: Array<{ seq: number; lookup: number }> }>>,
covCache: CoverageCache,
gidLookup: GidLookup,
): void {
const subStart = w.length;
const coverageHolder: number[] = [0];
@ -690,9 +807,10 @@ function writeChainFormat2(
}
}
/** Coverage 重映射:仅保留子集内 gidinput 第一分量必须在子集内才会被 shaping 命中) */
const origCovGids = readCoverageGids(r, coverageOff);
const newCovGids = origCovGids.map((g) => remapGid(origToNew, g)).filter((g): g is number => g !== null);
/** Coverage gidinput shaping
* readCoverageRemapped null coverage emitCoverage coverage
* map/filter */
const newCovGids = readCoverageRemapped(r, coverageOff, gidLookup, covCache) ?? [];
coverageHolder[0] = emitCoverage(w, newCovGids);
/** 重映射三个 ClassDef 的 gidclass index 不变) */
@ -721,7 +839,8 @@ function writeChainFormat2(
function parseChainFormat3(
r: Reader,
off: number,
origToNew: Map<number, number>,
covCache: CoverageCache,
gidLookup: GidLookup,
): { backCovs: number[][]; inputCovs: number[][]; lookCovs: number[][]; records: Array<{ seq: number; lookup: number }> } | null {
let p = off + 2;
const readCovArr = (): number[][] | null => {
@ -730,10 +849,9 @@ function parseChainFormat3(
const arr: number[][] = [];
for (let k = 0; k < count; k++) {
const covOff = off + r.u16(p + k * 2);
const gids = readCoverageGids(r, covOff);
const newGids = gids.map((g) => remapGid(origToNew, g)).filter((g): g is number => g !== null);
/** coverage 全部 gid 落在子集外,该 coverage 空 → 规则失效 */
if (newGids.length === 0 && gids.length > 0) return null;
const newGids = readCoverageRemapped(r, covOff, gidLookup, covCache);
/** coverage 全部 gid 落在子集外(原非空)→ 规则失效 */
if (newGids === null) return null;
arr.push(newGids);
}
p += count * 2;
@ -800,6 +918,8 @@ function serializeSubtable(
off: number,
type: number,
origToNew: Map<number, number>,
covCache: CoverageCache,
gidLookup: GidLookup,
): boolean {
r.clearError();
let ok: boolean;
@ -817,7 +937,7 @@ function serializeSubtable(
ok = serializeLigatureSubst(w, r, off, origToNew);
break;
case LT_CHAIN:
ok = serializeChainedContextSubst(w, r, off, origToNew);
ok = serializeChainedContextSubst(w, r, off, origToNew, covCache, gidLookup);
break;
default:
return false;
@ -841,6 +961,17 @@ export function subsetGSUB(
const dv = new DataView(gsubBytes.buffer, gsubBytes.byteOffset, gsubBytes.byteLength);
const r = new Reader(dv);
/** Coverage ChainContextSubst format3 coverage subtable
* off u16 FiraCode 604 /83 coverage */
const covCache: CoverageCache = new Map();
/** gid gid coverage Map.get
* =gid=gid-1 gid */
let maxOrigGid = 0;
for (const g of origToNew.keys()) if (g > maxOrigGid) maxOrigGid = g;
const gidLookup: GidLookup = new Int32Array(maxOrigGid + 1).fill(-1);
for (const [g, n] of origToNew) gidLookup[g] = n;
/** ---- GSUB Header ---- */
const major = r.u16(0);
const minor = r.u16(2);
@ -971,7 +1102,7 @@ export function subsetGSUB(
* copyBytesBlock subtable lookup
* lookup subtable */
const before = w.length;
const ok = serializeSubtable(w, r, lk.subtableAbsOffs[j], lk.effectiveType, origToNew);
const ok = serializeSubtable(w, r, lk.subtableAbsOffs[j], lk.effectiveType, origToNew, covCache, gidLookup);
if (!ok) {
w.rollback(before);
writeEmptySubtable(w, lk.effectiveType);

View File

@ -29,10 +29,13 @@ export class OTWriter {
return this.bytes.length;
}
/** 回退到指定字节位置,丢弃之后写入的字节与对应的偏移量槽(用于 subtable 重映射失败的保守降级) */
/** 退 subtable
* patches pos pop pos >= filter
* subsetGSUB subtable rollbackFiraCode 392 /callfilter pop */
rollback(pos: number): void {
this.bytes.length = pos;
this.patches = this.patches.filter((p) => p.pos < pos);
const patches = this.patches;
while (patches.length > 0 && patches[patches.length - 1].pos >= pos) patches.pop();
}
writeUint8(v: number): void {
@ -83,7 +86,11 @@ export class OTWriter {
*/
export class OTReader {
errorFlag = false;
constructor(private dv: DataView) {}
/** 原始 DataView热路径如 coverage 解析)可直接用 getUint16 绕过 u16 的逐次边界检查 */
readonly dv: DataView;
constructor(dv: DataView) {
this.dv = dv;
}
u16(off: number): number {
if (off < 0 || off + 2 > this.dv.byteLength) {