perf(gpos/gsub): read 零拷贝 subarray 替代 readBytes slice

GPOS/GSUB 表类 read 原用 reader.readBytes (= new Uint8Array(view.buffer, off, len).slice()),
.slice() 拷贝整个表字节(初夏 GPOS 68KB、GSUB 31KB)。V8 中 68KB slice 耗 ~40μs,
而 subarray 零拷贝仅 ~0.3μs(130× 差距)。

GPOS/GSUB 是原始字节透传:subsetGPOS/subsetGSUB 只读 OTReader + 写全新 OTWriter,
write 只 writeBytes 只读,optimize 不碰,整个 read→optimize→subset→write 生命周期
从不原地修改底层字节,故可安全返回共享 ArrayBuffer 的 subarray 视图。

初夏 full read 0.124→0.063ms (-49%),readBuffer 0.165→0.098ms。
全基准 SSIM 与输出字节完全不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-24 15:07:18 +08:00
parent ee7b6ec230
commit 8a7efea9d0
10 changed files with 286 additions and 2 deletions

61
_p_chxia.ts Normal file
View File

@ -0,0 +1,61 @@
import fs from 'node:fs';
import { Font } from './vendor/fonteditor-core/lib/ttf/font.js';
import { probeGsubAndCmap, collectReachableGsubTargets } from './backend/font_util/gsub-probe.js';
const fontBuf = fs.readFileSync('/mnt/d/字体资源/初夏明朝/初夏明朝-Regular.ttf');
const ab = fontBuf.buffer.slice(fontBuf.byteOffset, fontBuf.byteOffset + fontBuf.byteLength) as ArrayBuffer;
const text = ',。!?、;:“”‘’';
const codePoints = Array.from(text);
const option = { sourceType: 'ttf' as const, outType: 'woff2' as const };
function timeOnce<T>(label: string, fn: () => T, runs = 1): T {
// warmup
for (let i = 0; i < 3; i++) fn();
const t0 = performance.now();
let res: T;
for (let i = 0; i < runs; i++) res = fn();
const t1 = performance.now();
console.log(`${label}: ${((t1 - t0) / runs).toFixed(2)}ms`);
return res!;
}
// 阶段拆解best of 5
function median(arr: number[]) { return arr.slice().sort((a,b)=>a-b)[Math.floor(arr.length/2)]; }
function bench(label: string, fn: () => void, iters = 7) {
for (let i = 0; i < 3; i++) fn(); // warmup
const ts: number[] = [];
for (let i = 0; i < iters; i++) {
const t0 = performance.now();
fn();
ts.push(performance.now() - t0);
}
console.log(`${label}: ${median(ts).toFixed(3)}ms (min ${Math.min(...ts).toFixed(3)})`);
}
// 1. probe
bench('1.probe', () => probeGsubAndCmap(ab, codePoints, option.sourceType));
// 2. Font.create
bench('2.Font.create', () => Font.create(ab, { type: option.sourceType, subset: codePoints as any, kerning: true }));
// 完整 Font.create 拿到对象
const font = Font.create(ab, { type: option.sourceType, subset: codePoints as any, kerning: true });
// 3. optimize
bench('3.optimize', () => (font as any).optimize());
// 4. write (woff2)
const optimized = (font as any).optimize();
bench('5.write(woff2)', () => {
const f = Font.create(ab, { type: option.sourceType, subset: codePoints as any, kerning: true });
const o = (f as any).optimize();
o.write({ type: 'woff2', hinting: false } as any);
});
// 端到端对比
bench('E2E', () => {
const f = Font.create(ab, { type: option.sourceType, subset: codePoints as any, kerning: true });
const o = (f as any).optimize();
o.write({ type: 'woff2', hinting: false } as any);
}, 5);

9
_p_chxia_break.cjs Normal file
View File

@ -0,0 +1,9 @@
const fs = require('fs');
const path = require('path');
const ttf2woff2 = require('./vendor/fonteditor-core/lib/ttf2woff2.js');
const fontBuf = fs.readFileSync('./backend/font_util/标点测试字体/初夏明朝.ttf');
const text = ',。!?、:;';
// 复刻 fontSubset 的关键阶段
const { fontSubset } = require('./backend-ts-compiled.cjs');

31
_p_gposread.ts Normal file
View File

@ -0,0 +1,31 @@
import fs from 'node:fs';
const buf = fs.readFileSync('/mnt/d/字体资源/初夏明朝/初夏明朝-Regular.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset+buf.byteLength) as ArrayBuffer;
const view = new DataView(ab);
const numTables = view.getUint16(4, false);
let gposOff=0,gposLen=0;
for (let i=0;i<numTables;i++){const r=12+i*16;const tag=String.fromCharCode(view.getUint8(r),view.getUint8(r+1),view.getUint8(r+2),view.getUint8(r+3));if(tag==='GPOS'){gposOff=view.getUint32(r+8,false);gposLen=view.getUint32(r+12,false);break;}}
console.log('GPOS',gposOff,gposLen);
function median(a:number[]){return a.slice().sort((x,y)=>x-y)[Math.floor(a.length/2)];}
// 方式A: readBytes 的 slice 方式
const tA:number[]=[];
for(let i=0;i<30;i++){const t0=performance.now();const bytes=new Uint8Array(ab,gposOff,gposLen).slice();tA.push(performance.now()-t0);}
console.log(`slice(${gposLen}): ${median(tA).toFixed(4)}ms`);
// 方式B: new Uint8Array + set
const tB:number[]=[];
for(let i=0;i<30;i++){const t0=performance.now();const b=new Uint8Array(gposLen);b.set(new Uint8Array(ab,gposOff,gposLen));tB.push(performance.now()-t0);}
console.log(`new+set: ${median(tB).toFixed(4)}ms`);
// 方式C: subarray (零拷贝,但要确认后续不改原 buffer)
const tC:number[]=[];
for(let i=0;i<30;i++){const t0=performance.now();const b=new Uint8Array(ab,gposOff,gposLen);tC.push(performance.now()-t0);}
console.log(`subarray(view): ${median(tC).toFixed(4)}ms`);
// 方式D: copyBytesTo (Node 较新)
const tD:number[]=[];
const src=new Uint8Array(ab);
for(let i=0;i<30;i++){const b=new Uint8Array(gposLen);const t0=performance.now();src.copyBytesTo?src.copyBytesTo(b,0,gposOff,gposOff+gposLen):null;tD.push(performance.now()-t0);}
console.log(`copyBytesTo: ${median(tD).toFixed(4)}ms`);

50
_p_perTable.ts Normal file
View File

@ -0,0 +1,50 @@
import fs from 'node:fs';
import * as TTFReaderNS from './vendor/fonteditor-core/lib/ttf/ttfreader.js';
const TTFReader: any = TTFReaderNS.default.default ?? TTFReaderNS.default;
const support: any = (await import('./vendor/fonteditor-core/lib/ttf/table/support.js')).default.default ?? (await import('./vendor/fonteditor-core/lib/ttf/table/support.js')).default;
const buf = fs.readFileSync('/mnt/d/字体资源/初夏明朝/初夏明朝-Regular.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset+buf.byteLength) as ArrayBuffer;
const cps = Array.from(',。!?、;:“”‘’').map(c => c.codePointAt(0)!);
// 先拿到 reader + tables 目录
const r0 = new (TTFReader as any)({ subset: cps, kerning: true });
// 手动复刻 readBuffer 前半,拿 reader + tables
const Reader = (await import('./vendor/fonteditor-core/lib/ttf/reader.js')).default.default ?? (await import('./vendor/fonteditor-core/lib/ttf/reader.js')).default;
const Directory = (await import('./vendor/fonteditor-core/lib/ttf/directory.js')).default.default ?? (await import('./vendor/fonteditor-core/lib/ttf/directory.js')).default;
function makeReader() {
const reader = new Reader(ab, 0, ab.byteLength, false);
reader.offset = 12;
return reader;
}
// 各表独立计时
function timeTable(tableName: string, iters = 15) {
const TableClass = support[tableName];
if (!TableClass) { console.log(` ${tableName}: 无 support 类`); return; }
// warmup + 计时
const ts: number[] = [];
for (let i = 0; i < 5; i++) {
const reader = makeReader();
const dir = new Directory(12).read(reader, {});
if (!dir[tableName]) return;
const tc = new TableClass(dir[tableName].offset);
tc.read(reader, { tables: dir, readOptions: { subset: cps, kerning: true } } as any);
}
for (let i = 0; i < iters; i++) {
const reader = makeReader();
const dir = new Directory(12).read(reader, {});
const tc = new TableClass(dir[tableName].offset);
const t0 = performance.now();
tc.read(reader, { tables: dir, readOptions: { subset: cps, kerning: true } } as any);
ts.push(performance.now() - t0);
}
ts.sort((a,b)=>a-b);
console.log(` ${tableName}: ${ts[Math.floor(ts.length/2)].toFixed(3)}ms (min ${ts[0].toFixed(3)})`);
}
console.log('=== 初夏标点 各表 read 耗时 ===');
for (const t of ['head','hhea','maxp','OS/2','name','cmap','hmtx','vmtx','GPOS','GSUB','post']) {
timeTable(t);
}

37
_p_perTable2.ts Normal file
View File

@ -0,0 +1,37 @@
import fs from 'node:fs';
import * as TTFReaderNS from './vendor/fonteditor-core/lib/ttf/ttfreader.js';
const TTFReader: any = TTFReaderNS.default.default ?? TTFReaderNS.default;
const supportNS: any = await import('./vendor/fonteditor-core/lib/ttf/table/support.js');
const support: any = supportNS.default.default ?? supportNS.default;
const buf = fs.readFileSync('/mnt/d/字体资源/初夏明朝/初夏明朝-Regular.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset+buf.byteLength) as ArrayBuffer;
const cps = Array.from(',。!?、;:“”‘’').map(c => c.codePointAt(0)!);
// monkey-patch 每个表类的 read累加耗时
const stats: Record<string, number[]> = {};
for (const tn of Object.keys(support)) {
const TableClass: any = support[tn];
if (!TableClass || !TableClass.prototype || !TableClass.prototype.read) continue;
const orig = TableClass.prototype.read;
TableClass.prototype.read = function(reader: any, ttf: any) {
const t0 = performance.now();
const r = orig.call(this, reader, ttf);
const dt = performance.now() - t0;
(stats[tn] ??= []).push(dt);
return r;
};
}
function median(a: number[]) { return a.slice().sort((x,y)=>x-y)[Math.floor(a.length/2)]; }
// 多次跑 readBuffer 累加统计
for (let i = 0; i < 25; i++) {
const r = new (TTFReader as any)({ subset: cps, kerning: true });
r.readBuffer(ab);
}
console.log('=== 初夏标点 各表 read 耗时25次中位数===');
const rows = Object.entries(stats).map(([n, a]) => ({ n, med: median(a), min: Math.min(...a) }));
rows.sort((a,b)=>b.med-a.med);
for (const r of rows) console.log(` ${r.n}: ${r.med.toFixed(3)}ms (min ${r.min.toFixed(3)}, ${stats[r.n].length}次)`);

30
_p_read.ts Normal file
View File

@ -0,0 +1,30 @@
import fs from 'node:fs';
import * as TTFReaderNS from './vendor/fonteditor-core/lib/ttf/ttfreader.js';
const TTFReader: any = (TTFReaderNS as any).default.default ?? (TTFReaderNS as any).default ?? TTFReaderNS;
function median(a: number[]) { return a.slice().sort((x,y)=>x-y)[Math.floor(a.length/2)]; }
function bench(label: string, mk: () => () => void, iters = 9) {
const fn = mk();
for (let i = 0; i < 3; i++) fn();
const ts: number[] = [];
for (let i = 0; i < iters; i++) { const f = mk(); const t0 = performance.now(); f(); ts.push(performance.now() - t0); }
console.log(`${label}: ${median(ts).toFixed(3)}ms (min ${Math.min(...ts).toFixed(3)})`);
}
for (const [name, p, text] of [
['初夏标点', '/mnt/d/字体资源/初夏明朝/初夏明朝-Regular.ttf', ',。!?、;:“”‘’'],
['思源8字', './font/思源黑体.ttf', '天地玄黄宇宙洪荒'],
] as const) {
const buf = fs.readFileSync(p);
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset+buf.byteLength) as ArrayBuffer;
const cps = Array.from(text).map(c => c.codePointAt(0)!);
console.log(`\n=== ${name} ===`);
bench(' readBuffer', () => () => { const r = new (TTFReader as any)({ subset: cps, kerning: true }); r.readBuffer(ab); });
bench(' resolveGlyf', () => () => {
const r = new (TTFReader as any)({ subset: cps, kerning: true });
const ttf = r.readBuffer(ab);
r.resolveGlyf(ttf);
});
bench(' full read', () => () => { const r = new (TTFReader as any)({ subset: cps, kerning: true }); r.read(ab); });
}

29
_p_tables.ts Normal file
View File

@ -0,0 +1,29 @@
import fs from 'node:fs';
import * as TTFReaderNS from './vendor/fonteditor-core/lib/ttf/ttfreader.js';
const TTFReader: any = TTFReaderNS.default.default ?? TTFReaderNS.default;
function median(a: number[]) { return a.slice().sort((x,y)=>x-y)[Math.floor(a.length/2)]; }
for (const [name, p, text] of [
['初夏标点', '/mnt/d/字体资源/初夏明朝/初夏明朝-Regular.ttf', ',。!?、;:“”‘’'],
] as const) {
const buf = fs.readFileSync(p);
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset+buf.byteLength) as ArrayBuffer;
const cps = Array.from(text).map(c => c.codePointAt(0)!);
console.log(`\n=== ${name} (字体 ${ (buf.length/1024/1024).toFixed(1)}MB) ===`);
// 读 directory 拿各表大小
const view = new DataView(ab);
const numTables = view.getUint16(4, false);
const tables: {name:string; offset:number; length:number}[] = [];
for (let i = 0; i < numTables; i++) {
const rec = 12 + i*16;
const tag = String.fromCharCode(view.getUint8(rec), view.getUint8(rec+1), view.getUint8(rec+2), view.getUint8(rec+3));
const off = view.getUint32(rec+8, false);
const len = view.getUint32(rec+12, false);
tables.push({name: tag, offset: off, length: len});
}
tables.sort((a,b)=>b.length-a.length);
console.log('表大小 TOP:');
for (const t of tables.slice(0,12)) console.log(` ${t.name}: ${(t.length/1024).toFixed(0)}KB`);
}

28
_p_write.ts Normal file
View File

@ -0,0 +1,28 @@
import fs from 'node:fs';
import { Font } from './vendor/fonteditor-core/lib/ttf/font.js';
function median(a: number[]) { return a.slice().sort((x,y)=>x-y)[Math.floor(a.length/2)]; }
function bench(label: string, fn: () => void, iters = 9) {
for (let i = 0; i < 3; i++) fn();
const ts: number[] = [];
for (let i = 0; i < iters; i++) { const t0 = performance.now(); fn(); ts.push(performance.now() - t0); }
console.log(`${label}: ${median(ts).toFixed(3)}ms (min ${Math.min(...ts).toFixed(3)})`);
}
for (const [name, p, text] of [
['初夏标点', '/mnt/d/字体资源/初夏明朝/初夏明朝-Regular.ttf', ',。!?、;:“”‘’'],
['思源8字', './font/思源黑体.ttf', '天地玄黄宇宙洪荒'],
['令东千字', './font/令东齐伋复刻体.ttf', '天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈'],
] as const) {
const buf = fs.readFileSync(p);
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset+buf.byteLength) as ArrayBuffer;
const cps = Array.from(text);
console.log(`\n=== ${name} (${text.length}字) ===`);
bench(' create', () => Font.create(ab, { type: 'ttf', subset: cps as any, kerning: true }));
const font = Font.create(ab, { type: 'ttf', subset: cps as any, kerning: true });
const opt = (font as any).optimize();
bench(' write-woff2', () => opt.write({ type: 'woff2', hinting: false } as any));
// write-ttf 对比
bench(' write-ttf', () => opt.write({ type: 'ttf', hinting: false } as any));
}

View File

@ -15,7 +15,14 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
var _default = exports.default = _table.default.create('GPOS', [], {
read: function read(reader, ttf) {
var length = ttf.tables.GPOS.length;
return reader.readBytes(this.offset, length);
/* 328: subarray readBytes slice
* GPOS/GSUB 是原始字节透传subsetGPOS/subsetGSUB 只读 OTReader + 写全新 OTWriter
* write writeBytes 只读整个 readoptimizesubsetwrite 生命周期内从不原地修改
* 故无需 .slice() 拷贝68KB slice V8 ~40μssubarray ~0.3μs130× 差距
* readBytes 内部等价于 new Uint8Array(view.buffer, view.byteOffset+off, len).slice()
* 此处去掉末尾 .slice()直接返回共享底层 ArrayBuffer 的视图
* view.byteOffset 已由 reader 构造时设为传入 offsetTTFReader 0 readBytes 一致 */
return new Uint8Array(reader.view.buffer, reader.view.byteOffset + this.offset, length);
},
write: function write(writer, ttf) {
if (ttf.GPOS) {

View File

@ -18,7 +18,9 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
var _default = exports.default = _table.default.create('GSUB', [], {
read: function read(reader, ttf) {
var length = ttf.tables.GSUB.length;
return reader.readBytes(this.offset, length);
/* 328: subarray readBytes slice GPOS
* subsetGSUB 只读原始字节 + 写全新 OTWriterwrite 只读从不原地修改 */
return new Uint8Array(reader.view.buffer, reader.view.byteOffset + this.offset, length);
},
write: function write(writer, ttf) {
if (ttf.GSUB) {