mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 23:02:27 +08:00
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:
parent
ece6eeca2f
commit
308df8ef4d
8
_cmp.entry.ts
Normal file
8
_cmp.entry.ts
Normal 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
8
_cmp2.entry.ts
Normal 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
18
_probe_hmtx.entry.ts
Normal 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
44
_prof312.entry.ts
Normal 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
18
_prof_w2.entry.ts
Normal 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
33
_prof_woff2.entry.ts
Normal 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)}%)`);
|
||||
98
vendor/fonteditor-core/woff2/woff2-encode.js
vendored
98
vendor/fonteditor-core/woff2/woff2-encode.js
vendored
@ -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 次遍历(x→y→triplet)+ yCoords 数组;现 2 次遍历(x→y+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.14ms(3.6x),57168 点场景显著加速 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% CPU(prof 实测),
|
||||
* 函数调用与无法 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 回写到 flagAccum(flagStream 存 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user