diff --git a/_p_chxia.ts b/_p_chxia.ts new file mode 100644 index 0000000..04b1464 --- /dev/null +++ b/_p_chxia.ts @@ -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(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); diff --git a/_p_chxia_break.cjs b/_p_chxia_break.cjs new file mode 100644 index 0000000..6cea5c4 --- /dev/null +++ b/_p_chxia_break.cjs @@ -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'); diff --git a/_p_gposread.ts b/_p_gposread.ts new file mode 100644 index 0000000..3b43d8a --- /dev/null +++ b/_p_gposread.ts @@ -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;ix-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`); diff --git a/_p_perTable.ts b/_p_perTable.ts new file mode 100644 index 0000000..45a18de --- /dev/null +++ b/_p_perTable.ts @@ -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); +} diff --git a/_p_perTable2.ts b/_p_perTable2.ts new file mode 100644 index 0000000..84f6801 --- /dev/null +++ b/_p_perTable2.ts @@ -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 = {}; +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}次)`); diff --git a/_p_read.ts b/_p_read.ts new file mode 100644 index 0000000..867c364 --- /dev/null +++ b/_p_read.ts @@ -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); }); +} diff --git a/_p_tables.ts b/_p_tables.ts new file mode 100644 index 0000000..50a49a2 --- /dev/null +++ b/_p_tables.ts @@ -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`); +} diff --git a/_p_write.ts b/_p_write.ts new file mode 100644 index 0000000..31b2855 --- /dev/null +++ b/_p_write.ts @@ -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)); +} diff --git a/vendor/fonteditor-core/lib/ttf/table/GPOS.js b/vendor/fonteditor-core/lib/ttf/table/GPOS.js index ece5c9d..5c111f6 100644 --- a/vendor/fonteditor-core/lib/ttf/table/GPOS.js +++ b/vendor/fonteditor-core/lib/ttf/table/GPOS.js @@ -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 只读),整个 read→optimize→subset→write 生命周期内从不原地修改, + * 故无需 .slice() 拷贝(68KB slice 在 V8 耗 ~40μs,subarray ~0.3μs,130× 差距)。 + * readBytes 内部等价于 new Uint8Array(view.buffer, view.byteOffset+off, len).slice(), + * 此处去掉末尾 .slice(),直接返回共享底层 ArrayBuffer 的视图。 + * view.byteOffset 已由 reader 构造时设为传入 offset(TTFReader 传 0),与 readBytes 一致。 */ + return new Uint8Array(reader.view.buffer, reader.view.byteOffset + this.offset, length); }, write: function write(writer, ttf) { if (ttf.GPOS) { diff --git a/vendor/fonteditor-core/lib/ttf/table/GSUB.js b/vendor/fonteditor-core/lib/ttf/table/GSUB.js index cfc06b4..f5c87da 100644 --- a/vendor/fonteditor-core/lib/ttf/table/GSUB.js +++ b/vendor/fonteditor-core/lib/ttf/table/GSUB.js @@ -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 只读原始字节 + 写全新 OTWriter,write 只读,从不原地修改。 */ + return new Uint8Array(reader.view.buffer, reader.view.byteOffset + this.offset, length); }, write: function write(writer, ttf) { if (ttf.GSUB) {