perf(woff2): transformGlyfAndLoca 合并 y 解码与 triplet 编码为单循环

原 3 次遍历(x 解码→y 解码→triplet 编码)+ yCoords Int32Array 中转;
现 2 次遍历(x 解码→y 解码+triplet 编码合并)。

关键洞察:triplet delta 语义 = TTF delta(都是相对前一点的差),
所以解码 y 的同时用已存 xCoords[i] 配对编码 triplet,无需 yCoords 数组。
bbox_y 的 min/max 在合并循环同步计算,bbox bitmap 判定拆为 x/y 两段。

千字文 woff2 2.87ms→2.59ms(-9.6%),字节输出与 clean 版 cmp 完全一致,
SSIM 全部不变。删除 _reuseYCoords 模块级缓冲区。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-24 09:23:06 +08:00
parent ece6eeca2f
commit 308df8ef4d
7 changed files with 184 additions and 43 deletions

8
_cmp.entry.ts Normal file
View File

@ -0,0 +1,8 @@
import { readFileSync, writeFileSync } from 'fs';
import { fontSubset } from './backend/font_util/font.js';
const buf = readFileSync('font/令东齐伋复刻体.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const text = '天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔';
const out = fontSubset(ab, text, {sourceType:'ttf',outType:'woff2'});
writeFileSync('/tmp/woff2_opt312.bin', out);
console.log('len', out.length);

8
_cmp2.entry.ts Normal file
View File

@ -0,0 +1,8 @@
import { readFileSync, writeFileSync } from 'fs';
import { fontSubset } from './backend/font_util/font.js';
const buf = readFileSync('font/令东齐伋复刻体.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const text = '天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔';
const out = fontSubset(ab, text, {sourceType:'ttf',outType:'woff2'});
writeFileSync('/tmp/woff2_clean.bin', out);
console.log('len', out.length);

18
_probe_hmtx.entry.ts Normal file
View File

@ -0,0 +1,18 @@
import { readFileSync } from 'fs';
import { fontSubset } from './backend/font_util/font.js';
import supportMod from './vendor/fonteditor-core/lib/ttf/table/support.js';
const support = (supportMod as any).default || supportMod;
const hmtxT = support.hmtx;
const origRead = hmtxT.prototype.read;
hmtxT.prototype.read = function(reader: any, ttf: any) {
const r = origRead.call(this, reader, ttf);
if (ttf.subsetGids) {
let mx = 0;
for (const g of ttf.subsetGids) if (g>mx) mx=g;
console.log(`numGlyphs=${ttf.maxp.numGlyphs} subsetGids.len=${ttf.subsetGids.length} maxGid=${mx} alloc=${ttf.maxp.numGlyphs*2*4}B needed=${(mx+1)*2*4}B`);
}
return r;
};
const buf = readFileSync('font/令东齐伋复刻体.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
fontSubset(ab, '天地玄黄宇宙洪荒', {sourceType:'ttf',outType:'ttf'});

44
_prof312.entry.ts Normal file
View File

@ -0,0 +1,44 @@
import { performance } from 'perf_hooks';
import { readFileSync } from 'fs';
import { fontSubset } from './backend/font_util/font.js';
import supportMod from './vendor/fonteditor-core/lib/ttf/table/support.js';
const support = (supportMod as any).default || supportMod;
const buf = readFileSync('font/令东齐伋复刻体.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const times: Record<string, number> = {};
function wrap(proto: any, name: string, label: string) {
const orig = proto[name];
if (typeof orig !== 'function') return;
proto[name] = function(...args: any[]) {
const t = performance.now();
const r = orig.apply(this, args);
times[label] = (times[label]||0) + (performance.now()-t);
return r;
};
}
for (const tname of Object.keys(support)) {
const T = support[tname];
if (!T || !T.prototype) continue;
wrap(T.prototype, 'read', 'read:'+tname);
wrap(T.prototype, 'write', 'write:'+tname);
wrap(T.prototype, 'size', 'size:'+tname);
}
const text = '天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔';
for (const outType of ['ttf','woff2'] as const) {
for (const k of Object.keys(times)) delete times[k];
const opt = {sourceType:'ttf' as const, outType};
fontSubset(ab, text, opt);
const N = 200;
const t0 = performance.now();
for (let i=0;i<N;i++) fontSubset(ab, text, opt);
const total = performance.now()-t0;
console.log(`\n=== ${outType} total avg=${(total/N).toFixed(3)}ms ===`);
const sorted = Object.entries(times).sort((a,b)=>b[1]-a[1]);
for (const [k,v] of sorted.slice(0,12)) {
console.log(` ${k.padEnd(18)} ${(v*1000/N|0)/1000}µs ${(v/total*100).toFixed(1)}%`);
}
}

18
_prof_w2.entry.ts Normal file
View File

@ -0,0 +1,18 @@
import { fontSubset } from './backend/font_util/font.js';
import { readFileSync } from 'fs';
const buf = readFileSync('font/令东齐伋复刻体.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const text = '天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔';
const g:any = globalThis;
const N=200;
fontSubset(ab, text, {sourceType:'ttf',outType:'woff2'}); // warmup
g.__t.transform=0; g.__t.brotli=0;
import('perf_hooks').then(({performance})=>{
const t0=performance.now();
for(let i=0;i<N;i++) fontSubset(ab, text, {sourceType:'ttf',outType:'woff2'});
const total=performance.now()-t0;
console.log(`woff2 total avg=${(total/N).toFixed(3)}ms`);
console.log(` transformGlyfAndLoca: ${(g.__t.transform/N).toFixed(3)}ms (${(g.__t.transform/total*100).toFixed(1)}%)`);
console.log(` brotli: ${(g.__t.brotli/N).toFixed(3)}ms (${(g.__t.brotli/total*100).toFixed(1)}%)`);
console.log(` 其余(Font.create+ttf write+组装): ${((total-g.__t.transform-g.__t.brotli)/N).toFixed(3)}ms (${((total-g.__t.transform-g.__t.brotli)/total*100).toFixed(1)}%)`);
});

33
_prof_woff2.entry.ts Normal file
View File

@ -0,0 +1,33 @@
import { performance } from 'perf_hooks';
import { readFileSync } from 'fs';
import * as zlib from 'zlib';
const woff2enc = require('./vendor/fonteditor-core/woff2/woff2-encode.js');
const mod = woff2enc.__esModule ? woff2enc.default || woff2enc : woff2enc;
const tTransform = {total:0};
const tBrotli = {total:0};
// patch transformGlyfAndLoca
const origTransform = mod.transformGlyfAndLoca;
mod.transformGlyfAndLoca = function(...args:any[]) {
const t=performance.now();
const r = origTransform.apply(this,args);
tTransform.total += performance.now()-t;
return r;
};
// patch brotliCompressSync at module level not possible (local const), instead time encodeTTFToWOFF2 minus transform
const { fontSubset } = require('./backend/font_util/font.js');
const buf = readFileSync('font/令东齐伋复刻体.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const text = '天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔';
const N=200;
// warmup
fontSubset(ab, text, {sourceType:'ttf',outType:'woff2'});
const t0=performance.now();
for(let i=0;i<N;i++) fontSubset(ab, text, {sourceType:'ttf',outType:'woff2'});
const total=performance.now()-t0;
console.log(`woff2 total avg=${(total/N).toFixed(3)}ms`);
console.log(` transformGlyfAndLoca: ${(tTransform.total/N).toFixed(3)}ms (${(tTransform.total/total*100).toFixed(1)}%)`);
console.log(` 其余(brotli+组装): ${((total-tTransform.total)/N).toFixed(3)}ms (${((total-tTransform.total)/total*100).toFixed(1)}%)`);

View File

@ -187,11 +187,10 @@ for (let i = 124; i < 128; i++) TRIPLET_DATA_SIZES[i] = 4;
/* ======== glyf + loca 表变换 ======== */
/**
* 优化301: 模块级复用坐标缓冲区避免每个简单 glyph 分配 xCoords/yCoords Int32Array
* 优化301+312: 模块级复用 x 坐标缓冲区y 已合并进 triplet 循环不再需要 yCoords
* transformGlyfAndLoca 同步单线程调用复用安全按需扩容不释放
*/
let _reuseXCoords = new Int32Array(256);
let _reuseYCoords = new Int32Array(256);
/** 优化301: 复用 encode255UInt16 的 3 字节编码缓冲区 */
const _reuseEnc255 = [0, 0, 0];
@ -430,15 +429,17 @@ function transformGlyfAndLoca(glyfData, locaData, indexFormat, numGlyphs) {
}
}
/** 优化301: 复用模块级坐标缓冲区,避免每字形分配 Int32Array */
/** 312: x y triplet
* 3 次遍历xytriplet+ yCoords 数组 2 次遍历xy+triplet 合并
* 省掉 yCoords 数组分配/写入/读取 + 第三次 numPoints 遍历
* 关键洞察triplet delta 语义 = TTF delta都是相对前一点的差所以
* 解码 y 的同时用已存的 xCoords[i] 配对编码 triplet无需 yCoords 中转 */
if (numPoints > _reuseXCoords.length) {
const cap = _reuseXCoords.length;
const newCap = cap * 2 > numPoints ? cap * 2 : numPoints;
_reuseXCoords = new Int32Array(newCap);
_reuseYCoords = new Int32Array(newCap);
}
const xCoords = _reuseXCoords;
const yCoords = _reuseYCoords;
let px = 0;
let calcXMin, calcXMax;
/**
@ -446,7 +447,6 @@ function transformGlyfAndLoca(glyfData, locaData, indexFormat, numGlyphs) {
* 中文字体 87% 的点为 short 模式其中正负各半50/50
* 原三元 `(f & XSAME) ? b : -b` 50/50 不可预测分支 V8 编译成条件跳转导致流水线冲刷
* 改用 sign 位乘法 `(b * 2 - 1)` 消除分支XSAME=16(bit4)sign=(f>>4)&1
* 微基准0.49ms 0.14ms3.6x57168 点场景显著加速 transformGlyfAndLoca
*/
for (let xi = 0; xi < numPoints; xi++) {
const f = flagAccum[flagWriteBase + xi];
@ -466,30 +466,11 @@ function transformGlyfAndLoca(glyfData, locaData, indexFormat, numGlyphs) {
}
let py = 0;
let calcYMin, calcYMax;
for (let yi = 0; yi < numPoints; yi++) {
const f = flagAccum[flagWriteBase + yi];
if (f & YSHORT_FLAG) {
const b = glyfData[dataOff++];
py += b * (((f >> 5) & 1) * 2 - 1);
} else if (!(f & YSAME_FLAG)) {
let dy = (glyfData[dataOff] << 8) | glyfData[dataOff + 1];
if (dy > 0x7FFF) dy -= 0x10000;
py += dy;
dataOff += 2;
}
yCoords[yi] = py;
if (yi === 0) { calcYMin = py; calcYMax = py; }
else if (py < calcYMin) calcYMin = py;
else if (py > calcYMax) calcYMax = py;
}
let calcYMin = 0, calcYMax = 0;
if (numberOfContours > 0) {
const bboxMatches = calcXMin === xMin && calcYMin === yMin && calcXMax === xMax && calcYMax === yMax;
if (!bboxMatches) {
bboxBitmap[gi >> 3] |= (0x80 >> (gi & 7));
bboxStreamSize += 8;
}
/** 优化312: bbox bitmap 判定拆分——x 在此先判y 在合并循环算完后补判 */
let bboxSet = !(calcXMin === xMin && calcXMax === xMax);
/**
* 优化294: triplet 数据直接追加写入 glyphAccum连续累积不再分配 per-glyph glyphStreamBuf
@ -505,26 +486,41 @@ function transformGlyfAndLoca(glyfData, locaData, indexFormat, numGlyphs) {
const gsBase = glyphAccumLen;
let gsbi = 0;
let prevX = 0, prevY = 0;
/**
* 优化303: calcTripletAndWrite 手动 inline 到主循环消除 54350 /call 函数调用开销
* 千字文 54350 点场景calcTripletAndWrite transformGlyfAndLoca 31.8% CPUprof 实测
* 函数调用与无法 inline 的参数装箱是主因inline V8 可在循环内做寄存器分配 +
* 公共子表达式消除absDx/absDy/curveBit 跨分支复用语义与原 calcTripletAndWrite 完全一致
* 分支顺序保持原样特例 yOnly/xOnly 优先千字文 78% 命中 1B 双轴分支
*/
const _gs = glyphAccum;
for (let pi = 0; pi < numPoints; pi++) {
const cx = xCoords[pi];
const cy = yCoords[pi];
/** 优化302: curveBit 无分支——onCurve(flag&1=1)→0, 控制点(flag&1=0)→128 */
const curveBit = ((flagAccum[flagWriteBase + pi] & 1) ^ 1) << 7;
const _fa = flagAccum;
const _fwb = flagWriteBase;
const _gd = glyfData;
let dyBboxUnset = true;
/**
* 优化312: y 解码 + triplet 编码合并为单循环
* py TTF yCoord 字节流解码累积绝对坐标cx xCoords x 已在前一循环解好
* triplet delta = cx - prevX / py - prevY当场编码写入 glyphAccum
* bbox_y min/max 也在本循环同步计算原在独立 y 循环
*/
for (let yi = 0; yi < numPoints; yi++) {
const f = _fa[_fwb + yi];
if (f & YSHORT_FLAG) {
const b = _gd[dataOff++];
py += b * (((f >> 5) & 1) * 2 - 1);
} else if (!(f & YSAME_FLAG)) {
let dy0 = (_gd[dataOff] << 8) | _gd[dataOff + 1];
if (dy0 > 0x7FFF) dy0 -= 0x10000;
py += dy0;
dataOff += 2;
}
if (dyBboxUnset) { calcYMin = py; calcYMax = py; dyBboxUnset = false; }
else if (py < calcYMin) calcYMin = py;
else if (py > calcYMax) calcYMax = py;
/** triplet 编码(与原 calcTripletAndWrite inline 语义一致) */
const cx = xCoords[yi];
const cy = py;
const curveBit = ((f & 1) ^ 1) << 7;
const dx = cx - prevX;
const dy = cy - prevY;
const absDx = dx < 0 ? -dx : dx;
const absDy = dy < 0 ? -dy : dy;
const wpos = gsBase + gsbi;
/** triplet flag flagAccum + TRIPLET_DATA_SIZES
* 默认值仅占位每个分支都会覆盖 */
let flag;
if (dx === 0 && absDy < 1280) {
_gs[wpos] = absDy & 0xFF;
@ -564,11 +560,17 @@ function transformGlyfAndLoca(glyfData, locaData, indexFormat, numGlyphs) {
flag = curveBit + 124 + xSignBit + 2 * ySignBit;
}
/** triplet flag 回写到 flagAccumflagStream 存 triplet flag 而非原始 flag */
flagAccum[flagWriteBase + pi] = flag;
_fa[_fwb + yi] = flag;
gsbi += TRIPLET_DATA_SIZES[flag & 0x7F];
prevX = cx;
prevY = cy;
}
/** 优化312: y 的 bbox 匹配补判 */
if (calcYMin !== yMin || calcYMax !== yMax) bboxSet = true;
if (bboxSet) {
bboxBitmap[gi >> 3] |= (0x80 >> (gi & 7));
bboxStreamSize += 8;
}
glyphAccumLen += gsbi;
glyphStreamSize += gsbi;
@ -582,6 +584,16 @@ function transformGlyfAndLoca(glyfData, locaData, indexFormat, numGlyphs) {
}
for (let e = 0; e < n; e++) glyphAccum[glyphAccumLen++] = _reuseEnc255[e];
glyphStreamSize += n;
} else {
/** numberOfContours === 0 的空字形:仍需消费 yCoord 字节以推进 dataOff保持原语义 */
for (let yi0 = 0; yi0 < numPoints; yi0++) {
const f = flagAccum[flagWriteBase + yi0];
if (f & YSHORT_FLAG) {
dataOff++;
} else if (!(f & YSAME_FLAG)) {
dataOff += 2;
}
}
}
glyphInfos[gi] = numberOfContours > 0