131 Commits

Author SHA1 Message Date
崮生(子虚)
852f0d2f21 perf(ot-bytes): OTWriter 初始容量 256→2048 + 内联容量检查消除热路径函数调用
subsetGPOS 思源 36→33μs、subsetGSUB 13→11μs;writeUint16 是序列化第一热点
(思源 207次/call),原 per-call ensure() 函数调用在 V8 class private method
不内联;内联为容量判断 + grow,并预分配 2048 避免小输出多次扩容拷贝
2026-07-24 14:40:05 +08:00
崮生(子虚)
04bf5c6077 perf(os2): size 固定返回 96 跳过通用 struct 查表(version 恒 0x4) 2026-07-24 14:33:54 +08:00
崮生(子虚)
82c1e42108 perf(name): subset 字体 name 表白名单,仅保留渲染必需 6 个 nameId
subset 字体核心目的是小体积下载 + 正确渲染,但 name 表保留了全部 19 个 nameId
(含版权/商标/URL/版本/描述等元数据)。思源 8 字 ttf 8.8KB 里 name 占 3.9KB(44%),
多为这些 web 渲染无用的元数据,徒增 brotli 输入与下载体积。

size 阶段加 KEEP_NAME_IDS 白名单,只保留浏览器字体匹配/渲染必需的:
  1 fontFamily / 2 fontSubFamily(CSS font-family 与 weight/style 匹配核心)
  4 fullName / 6 postScriptName(全名与 PS 名)
  16 preferredFamily / 17 preferredSubFamily(变体/typographic 匹配)

体积收益(SSIM 全部不变,渲染像素不受 name 内容影响):
  思源 8 字 woff2 4448→2632B(-41%)、思源千字文中段 19100→17156B(-10%)
  FiraCode 11636→10828B(-7%)、思源标点 4864→3048B(-37%)
令东千字文 -0.5%(name 占比本就小)。brotli 输入减小附带压缩提速。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 14:22:11 +08:00
崮生(子虚)
089ae7c0a7 perf(name): read 单遍扫描定 platform,消除全部中间 nameRecord 对象
原 read 先建 count 个 {platform,encoding,language,nameId,length,offset} 中间对象,
再两遍遍历它们。改为两趟直扫 view:
  pass1 只读 platform/encoding/language 三字段比较定 platform(windows english 优先),
        不读 nameId/length/offset、不建对象;
  pass2 头开始只对匹配记录读 nameId/length/offset + readBytes + 解码,直接产出 names。

思源 56 条记录中间对象 56→0,view 读次数减半。字节 sha256 完全一致,
5 轮交替 readBuffer median 稳定 -3~-5%(对象分配本身 V8 近零成本,收益小但一致)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 12:53:04 +08:00
崮生(子虚)
ba17974e1d perf(name): read 延迟 readBytes,按需只解码选中 platform 的记录
原 name.read 第一遍循环对全部 count 条记录都 readBytes(每次 slice 一份字节),
但最终第三循环只对匹配 platform+encoding+language 且 nameId 有用的记录解码字符串,
其余记录的 .name 字节从不被消费。思源 56 条记录只解码约 15 条,浪费 41 次 slice。

改为删除第一遍全量 readBytes,在第三循环里对匹配记录按需 readBytes,
N 次 slice 降到「有用记录数」次。字节 sha256 完全一致(思源/令东验证),
思源 readBuffer median 0.021→0.014ms(-34%),令东持平(name 表小本就无浪费)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 12:50:28 +08:00
崮生(子虚)
d6d94e201e perf(os2): read 改用直接 DataView 批量读取
OS/2.read 原走 struct 通用 read(逐字段 switch 分发 + reader.read 调用),
与已优化的 write(优化176 直接 view)不对称。改为手写每个字段的 view 直读,
按 version(0/1/2+)决定读到哪,补默认值逻辑不变。

字段值完全一致(version/achVendID/sxHeight 验证),OS/2.read 微基准
思源 6.2→1.7μs(-73%)、令东 5.5→0.6μs(-89%),端到端约 -1%
(readBuffer 占比小),与 write 对称后可推广到其他 struct 通用 read 表。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 12:44:50 +08:00
崮生(子虚)
d575d55540 perf(name): 合并 UTF-8+UCS-2 编码 + ASCII 快路径
name 表 size 阶段对每条记录都要 UTF-8(mac)+ UCS-2(windows)各一份,
原两次独立编码各做一遍 stringify 扫描 + 编码循环。新增 toUTF8AndUCS2Bytes
合并为单次扫描:纯 ASCII(含拒绝 \0)走超快路径——UTF-8 直接取字节、
UCS-2 大端序 [0,ch],省掉多字节分支与两次扫描;非 ASCII 回退原逻辑。

字节 sha256 完全一致(思源/令东验证),思源 8 字 woff2 端到端 -7%
(name.size 占 dump 44%),令东千字文持平(name 占比 0.3% 在噪声内)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 12:38:40 +08:00
崮生(子虚)
bcd4574b3f perf(woff2): triplet 编码无分支 abs + 符号位提取
transformGlyfAndLoca 的 y+triplet 循环每点原先 4 次三元判断(absDx/absDy/xSign/ySign),
中文字体 dx/dy 正负近似 50/50 为不可预测分支,致流水线冲刷。

改用 Int32 算术右移统一提取符号:
- dxSgn = dx >> 31(0 或 -1)
- absDx = (dx ^ dxSgn) - dxSgn(无分支 abs)
- xSignBit = dxSgn + 1(等价 dx >= 0 ? 1 : 0)

令东 74.6% 点走 case3,收益最大:woff2 端到端 2.197→2.078ms(-5.4%),
思源 0.662→0.653ms(-1.4%)。sha256 字节级一致,全基准 SSIM 零变化。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 12:08:48 +08:00
崮生(子虚)
60db8d226b perf(woff2): triplet 编码 case2/3/4 去掉冗余的轴非零判定
case1 排除 dx===0、case2 排除 dy===0 后,走到 case3/case4 时两轴必非零,
故 case3/case4 的 'dx!==0 && dy!==0' 是冗余判定。令东 74.6% 点命中 case3,
每点省 2 次比较。case2 的 'dx!==0' 同理冗余(case1 未命中且 dy===0 则 dx 必非0)。
cmp 字节一致,逻辑等价已证明。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:55:24 +08:00
崮生(子虚)
72c6afef92 perf(woff2): 移除 simple glyph 的 bbox 计算,由解码端从轮廓点重建
WOFF2 规范:bboxBitmap bit=0 时解码端计算点的 min/max 重建 bbox,bit=1 读显式存储值。
原编码对 simple glyph 计算 calcBbox 并与原始 bbox 比对决定 bit——但置 1 时存的也是
calcBbox,故无论 bit 值解码端都得 calcBbox,该计算与比对无功能意义。

移除后 simple glyph 永远 bit=0(解码端自算),省掉 x/y 循环每点 3 次 bbox 比较 + bboxSet
判定 + Pass2 写入。复合 glyph 仍显式存 bbox(raw 数据剥离了 glyph 头,解码端无法重建)。

令东千字文 woff2 write 2.449→2.237ms,输出体积 -20B~60B,全基准 SSIM 零变化。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:51:29 +08:00
崮生(子虚)
f945e1aedb perf(woff2): triplet 编码各分支内累加 gsbi,消除 TRIPLET_DATA_SIZES 查表
令东千字文 woff2 write -12%(2.785→2.449ms),输出字节完全一致(cmp sha256 验证)。
原 gsbi += TRIPLET_DATA_SIZES[flag & 0x7F] 每点查表(Uint8Array 索引+位运算),
改为各 case 分支内 adv=1/1/1/2/3/4 常量累加,省掉查表。思源 -4%。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:48:12 +08:00
崮生(子虚)
aeb2622a57 perf(gsub-reachable): format3 失败路径跳过 contextGids 收集
format3 triggerable=false 是常态(初夏纯标点 280 个 format3 首轮全 false)。
失败时只需找首个不在 reachable 的 gid 记 failGid——此刻 contextGids 收集
无意义:break 前已检查的 gid 都在 reachable(inSubset=reachable.has),
调用方对其 reachable.add 是 no-op。改用 coverageFirstExcludedGid(复用
readCoverageGids 带 cache + 遍历短路)分离失败/成功两遍,失败路径省掉
contextGids.add 调用。仅 triggerable=true(coverage 全在子集、量小)时
第二遍收集 contextGids。

reachable 输出集合逐元素一致(初夏 23/FiraCode 89),全基准 SSIM 不变。
初夏 reach 0.265→0.245ms(-8%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:28:34 +08:00
崮生(子虚)
3103dd5db1 perf(gsub): ScriptList/FeatureList 整块拷贝快路径(同 GPOS)
subsetGSUB 复用 scriptListSpan/featureListSpan,span 自洽且不越过相邻 list
header offset 时整块 subarray 拷贝。交错判定用 SL span≤featureListOff、
FL span≤lookupListOff(LookupList subtable 散落,header offset 不能代表其
字节范围,故不用「下一更大 offset」作上界)。霞鹜文楷 GSUB 物理交错
(LL<SL<FL 且 ScriptTable 跨越 FL) 自动降级 serialize,字节与优化前完全一致。

收益:初夏 GSUB 11474→8474B(-26%)、FiraCode 15504→14878B(-4%),
保留 fontTools FeatureTable 去重。霞鹜降级路径 cmp 逐字节一致。全基准 SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:23:06 +08:00
崮生(子虚)
ccc2e995c8 perf(gpos): ScriptList/FeatureList 整块拷贝跳过逐字段序列化
两表不含 glyphId(仅引用 lookup index),绝大多数字体其子表紧凑排列在
[listOff, listOff+span) 内、与下一表无物理交错,字节块本身即合法表。
新增 scriptListSpan/featureListSpan 计算跨度并校验自洽,满足则 subarray
整块拷贝,否则降级 serializeScriptList/serializeFeatureList。

收益:初夏 subsetGPOS 0.270→0.226ms,纯标点 0.240→0.165(-31%),
思源 0.071→0.036(-49%)。顺带保留 fontTools 的 FeatureTable 去重
(serialize 重排丢失致初夏 FeatureList 1840→3728B),输出 GPOS 字节
初夏 12064→10176(-16%)、思源 1194→978(-18%)。全基准 SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:16:21 +08:00
崮生(子虚)
8c311768f6 perf(gsub-reachable): format3 失败 gid 跨轮跳过冗余扫描
不动点迭代中 format3 triggerable=false 的 subtable,记录使其失败的第一个
coverage gid。下一轮若该 gid 仍未进 reachable,本 subtable 必定仍 false,
直接跳过 collectChainRefs 重扫。

原 settledChain 只记忆稳定 subtable(format2 / format3 triggerable=true),
format3 triggerable=false 每轮都重扫。初夏纯标点 280 个 format3 全 false,
3 轮 × 280 = 840 次 collectChainRefs;改后 chainCalls 280→(第 2 轮起全跳过),
-67%。失败 gid 进 reachable 时重扫(可能仍 false 则更新失败 gid,可能变 true
则转 settledChain)。

reachable 集合字节级一致(初夏/得意黑/思源全 cmp 通过),SSIM 全程不变。
初夏 reach 0.33→0.29ms(-13%,collectChainRefs 非 reach 全部开销故收益有限)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:05:52 +08:00
崮生(子虚)
65e238cdca perf(cmap): subset 模式 format4 延迟解析,跳过 4×segCount 数组构建
subset 模式下 format4 不再急切构建 endCode/startCode/idDelta/idRangeOffset
共 4 个 segCount 项数组,只记录各段在 view 的偏移;lookupFormat4 二分时
按需从 view 读(mid*2 偏移)。原 subset 模式仍执行 4×segCount 次 getUint16
+ 4 个数组分配,得意黑 segCount=3929 → 1.5 万次读,占 readSubTable 0.101ms。
延迟后 view 读次数 = O(S × log(segCount)),远小于 4×segCount。

lookupFormat4 新增 _lazySegs 分支,非延迟路径(全量模式)保持急切数组不变。
idDelta 仍用 getUint16 读(与原急切模式一致),依赖 % 0x10000 处理符号。

字节逐字节一致(令东/思源/初夏/得意黑/otf 全 cmp 通过),SSIM 全程不变。
cmap: 初夏 0.075→0.009ms(-88%),得意黑→0.004ms(-94%),思源 0.063→0.014(-78%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:57:36 +08:00
崮生(子虚)
b7dbe3fe1d perf(hmtx): subset 模式紧凑存储,消除 numGlyphs*2 大对象分配
subset 模式下 hmtx 改为按 subsetGids 顺序紧凑存储 Int32Array(S*2),
下标用 gi*2(subsetGids 内位置)替代 gid*2。原实现分配 Int32Array(numGlyphs*2),
CJK 字体 numGlyphs 达 3.5 万→284KB 大对象,V8 大对象分配+zero-fill 单次 ~54μs。

resolveGlyf subset 分支同步改用 gi*2 索引。OTF 路径不变(表顺序 hmtx 在 CFF 前,
subsetGids 未就绪,仍走全量数组)。

字节逐字节一致(令东/思源/初夏/得意黑/otf 全 cmp 通过),SSIM 全程不变。
初夏 hmtx 0.056→0.002ms(-96%),create -15%,fontSubset 总 -7%。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:52:30 +08:00
崮生(子虚)
aa2818b13e perf(gsub): gidLookup 改 Int32Array,大数组 fill 快 3×
GidLookup(原gid→新gid 查找表)原用 number[].fill(-1), TypedArray 的 fill
是 native memset,比 number[].fill 快约 3×(35515 项:219μs→67μs)。

初夏明朝 subsetGids 仅十余个但 maxOrigGid 达 3.5 万(reachable 的高 gid target),
number[].fill(-1) 耗时 ~200μs,占 subsetGSUB 总耗时 ~47%。Int32Array.fill
降至 ~67μs,索引访问速度实测与 number[] 一致(FiraCode 密集查询场景同 8μs)。

初夏 rewrite 0.663→0.501ms(-24.5%),总 fontSubset -9.2%;FiraCode 略快 -2.8%。
GSUB 输出字节级一致(cmp 通过),30 项基准 SSIM 零变化。

推翻记忆 gsub-subset-perf-coverage-cache 中「number[] 比 Int32Array 快 2×」结论
(旧 V8 或不同测试条件),当代 V8 下两者访问速度相同,TypedArray fill 显著更快。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:38:27 +08:00
崮生(子虚)
6d897f1987 perf(gsub-reachable): collectChainRuleRefs 消除 3 个临时数组分配
ChainSubRule 解析原先读 backtrack/input/lookahead 到 3 个 number[] 再两遍遍历
(先 inSubset 校验再 contextGids.add),format1 每 rule 分配 3 数组
(FiraCode 424 次/call × 3 = 1272 次数组分配 + GC 压力)。

改为两遍扫描同一段字节,不分配中间数组:
- 第一遍仅 inSubset 校验全部 gid(遇子集外即 return,放弃规则)
- 通过校验后再第二遍收集 context gid + 读 subst records

reachable 输出字节级一致(FiraCode 132 / 初夏 63 均通过 cmp),
FiraCode reach 0.368→0.343ms(-6.8%),30 项基准 SSIM 零变化。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:30:05 +08:00
崮生(子虚)
dd4f7ba55a perf(gsub-subset): format3 预检用 readCoverageRemapped 保证判定一致
opt317 的 format3 预检用 coverageAllOutOfSubset 内联判定,其 COV_RANGE
累积超限逻辑与 parseChainFormat3 实际用的 readCoverageRemapped 不一致,
导致 FiraCode 13 个 format3 预检未跳过却深度解析失败(0.328ms/call 浪费)。

预检改用 readCoverageRemapped(与深度解析同一判定函数 + 共享 covCache),
精确保证「预检跳过 ⟺ 深度解析失败」,输出完全一致(都是空 subtable)。
readCoverageRemapped 触碰 covCache 无副作用(gids 留 EMPTY_GIDS 占位,
readCoverageGids 命中按 miss 重算,已有逻辑)。

另:writeEmptySubtable 的 coverageOffset 恒为 6,改直接写常量,
省 reserveOffset16 的 patches push + 闭包分配。

FiraCode subsetGSUB 1.049→0.768ms(-27%),fontSubset 2.49→2.27ms(-9%)。
GSUB/woff2 字节 cmp 全绿,30 项基准 SSIM 零变化(FiraCode 0.9923、
千字文 0.9986、初夏纯标点 0.9920、otf 0.9681/0.9310 均不变)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:14:38 +08:00
崮生(子虚)
1da81eb0c0 chore(gsub-subset): 清理 profiling 残留的 __sp 插桩
上一轮 opt317 提交时误带入了 format1/format3 序列化路径上的 __sp 计时
插桩(globalThis 属性读取 + performance.now 调用)。热路径每个 type6
subtable 都执行,属无关生产代码。删除并移除无用的 perf_hooks import。

GSUB 字节输出零变化(cmp 一致),性能持平。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:02:46 +08:00
崮生(子虚)
4c9022adb6 perf(gsub-subset): format3 预检判定修正跳过不可能触发的规则
subsetGSUB 真 hot path(FiraCode 2.2ms)是 type6 format3 失败路径占 69%
(16 次×0.094ms)。根因:isSubtableSkipableByCoverage format3 旧判定
「整组 coverage 全子集外」语义过强,当一组内部分 coverage 全子集外、
部分含子集内 gid 时预检不跳过,但 parseChainFormat3 遇到那个全子集外的
coverage 即返回 null 失败,白白深度解析。

format3 规则触发需三组 coverage 的 gid 全在子集内,故任一 coverage 原非空
且全子集外(coverageAllOutOfSubset===true,等价 readCoverageRemapped===null)
即不可能触发,预检直接跳过。

FiraCode fontSubset 3.751→2.029ms(-46%)。GSUB 字节 cmp 完全一致,
woff2 端到端字节 cmp 全绿,SSIM 0.9923 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:55:49 +08:00
崮生(子虚)
56e8b6db4e perf(gsub-reachable): format3 闭包消除 + 固定点迭代稳定性记忆
collectChainRefs 在固定点迭代中高频调用(初夏纯标点 280 个 format3 subtable ×
多轮),每次创建 readCovGidsChecked 闭包有分配开销。

优化315: format3 分支消除闭包,改显式三段循环手动推进 p,保留 triggerable 短路。
优化316: collectChainRefs 返回稳定标志(format2 总稳定 / format3 triggerable=true
稳定),主循环用 settledChain 记忆已稳定 subtable 偏移,后续轮直接跳过。

稳定测量(N=300 预热):
  FiraCode       3.757 → 3.529ms (-6%)
  初夏-纯标点     1.847 → 1.548ms (-16%)
  初夏-汉字标点   1.503 → 1.260ms (-16%)
reach 阶段(单测): FiraCode 0.449→0.318ms, 初夏 0.544→0.315ms
无 GSUB 字体(令东/思源)无影响。所有用例 reach 输出与黄金基准逐字节一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:43:55 +08:00
崮生(子虚)
a1c7312722 perf(glyf): _origGlyfRef 子对象展平为 _origBuf/_origOff/_origLen
subset 快路径每个 simple 字形原本分配 glyfObj + _origGlyfRef 两个对象,
展平后 _origGlyfRef 的 {buffer,byteOffset,length} 直接存为 glyfObj 的三个属性,
省掉每字一个子对象分配 + 下游解引用间接寻址。

改 glyf.js(声明+component 守卫)、write.js、sizeof.js、optimizettf.js。
千字文 ttf 0.246→0.234ms(-5%),ttf/woff2 字节输出 cmp 完全一致,SSIM 全部不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:31:43 +08:00
崮生(子虚)
6b28ba3d74 perf(glyf): subset 快路径删 endPtsOfContours 数组分配,直接读最后 endPt
优化310 快路径下 endPtsOfContours 数组仅用于算 _totalPoints(供 maxPoints metrics),
下游 optimizettf 快路径分支、sizeof/write 原始字节拷贝均不消费它。
直接读最后一个 endPt(vOff+10+(numContours-1)*2)算 _totalPoints,
省掉每个 simple 字形的 Array 分配 + numContours 次 getUint16 循环。

千字文 ttf 0.252→0.246ms,ttf 字节输出 cmp 完全一致,SSIM 全部不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:27:19 +08:00
崮生(子虚)
308df8ef4d 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>
2026-07-24 09:23:06 +08:00
崮生(子虚)
ece6eeca2f perf(buffer): toBuffer ArrayBuffer→Buffer 用 Buffer.from 零拷贝替代逐字节循环
千字文 ttf 0.396ms→0.252ms(-36%),woff2 不受影响(不走 node Buffer 路径)。
Writer 的 ArrayBuffer 精确分配为 ttfSize(offset=0),getBuffer 返回完整 buffer,
Buffer.from 共享底层内存无 over-exposure 风险。SSIM 全部不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:15:17 +08:00
崮生(子虚)
69dc5e343e perf(glyf): simple 字形原始字节快路径,千字文 ttf -80%/woff2 -35%
子集化 simple 字形经 parse→optimizettf→write 往返后产出的 glyf 字节
与原始字体完全一致(实测 73/73),直接拷贝原始字节跳过三段往返。
- glyf.js subset read: simple 字形存 _origGlyfRef 引用 + 读 header/endPts
  供 metrics,跳过坐标解码;compound 及 component 走完整 parse
- optimizettf: 检测 _origGlyfRef 只收集 metrics 跳过 ceilReduce
- sizeof: glyfSize = 原始字节长度
- write: fullView.set 原始字节段跳过 encode

额外保留原始 hinting instructions(原非 hinting 模式丢弃),渲染更精确。
SSIM 全部不变(千字文 0.9986/FiraCode 连字 0.9923/初夏明朝 0.9920)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:08:22 +08:00
崮生(子虚)
8f0964ce54 perf(parse): OS/2 表类按 format 缓存 + 清理 dead write,千字文 -5%
OS/2 read 原每次 struct.slice + _table.create 动态建类(_createClass +
Object.assign prototype),是 OS/2 parse 主开销(prof 稳态 ObjectAssign 4.2%)。
format 仅 0/1/2+ 三种,表类按 format 模块级缓存复用;去掉 Object.assign
(os2Fields, tbl),直接在 tbl 上补默认字段。

附带清理:ceilReduceAndSizeFromTypedArrays/Flat 的 _preXLen/_preYLen 是
全库零读取点的 dead write(subarray.length 已携带长度),删除;
optimizettf 后补 _yArr=null(悬挂内存,唯一消费者因 _xArr=null 守卫进不去)。

千字文 woff2 A/B:4.47→4.26ms(-5%),字节完全相同,SSIM 全部一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 08:51:44 +08:00
崮生(子虚)
78e8f798e8 perf(woff2): Brotli 质量 q3→q2,再省 8% 时间,体积仅 +0.09%
实测千字文 woff2 各 quality 端到端时间 + 体积(同文本):
  q3=4.65ms/42712B  q2=4.26ms/42752B  q1=4.21ms/46432B  q0=4.12ms/47240B
q2 是时间/体积最优拐点:比 q3 再省 8% 时间,体积仅 +40 字节(+0.09%)。
q1/q0 体积陡增 +8.7%/+10.6%(下载/解析代价),不划算。

基准测试全部 SSIM 与 ink 计数一致(FiraCode 0.9923、初夏明朝纯标点 0.9920)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 08:42:36 +08:00
崮生(子虚)
4fb4679636 perf(woff2): Brotli 质量 q6→q3,千字文 -26%/FiraCode -13%,SSIM 不变
WOFF2 是无损容器,Brotli 质量只影响压缩率不影响解码结果。
实测 glyf 变换数据:q6=3.66ms/45935B(54.0%) → q3=1.20ms/46168B(54.3%),
时间 -67%,体积仅 +0.5%(持平)。q3 是时间/体积最优点,再降体积显著增大。

端到端 A/B(esbuild bundle):千字文 woff2 6.31→4.65ms(-26%),
FiraCode woff2 11.80→10.27ms(-13%)。基准测试全部 SSIM 与 ink 计数完全一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 08:37:55 +08:00
崮生(子虚)
8fd11fc859 perf(woff2): calcTripletAndWrite inline 到主循环,消除每点函数调用
千字文 woff2(54350 点)transformGlyfAndLoca 的 calcTripletAndWrite 占 31.8% CPU
(V8 --prof 实测)。手动 inline 到 triplet 循环消除 54350 次/call 调用开销,
让 V8 做循环内寄存器分配与公共子表达式消除。原独立函数删除(避免死代码)。

输出字节逐字节完全一致(千字文/思源黑体/FiraCode 三组 IDENTICAL 验证)。
微基准 encodeTTFToWOFF2 千字文 4.84→4.38ms(-9.5%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 07:27:09 +08:00
崮生(子虚)
d68551d23e feat(stats): 统计计数跨重启持久化 + 依赖升级
- 新增 stats_store.ts:防抖写盘 font/.stats.json,热路径零磁盘 IO
- shared.ts 暴露 initStats/markStatsDirty/snapshotStats
- app.ts 启动恢复累计计数 + 退出前尽力落盘
- 路由热路径打脏标记(防抖 30s 批量落盘)
- 清理未用变量/导入/常量
- 升级依赖:vue 3.6.0-beta.17、typescript 7.0.2、vite 8.1.5 等

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 21:43:12 +08:00
崮生(子虚)
f7a2a4dc86 perf(gsub-reachable): collectChainRefs format3 短路 + 去闭包/中间数组
固定点迭代中 collectChainRefs 高频调用(初夏纯标点 840 次/call)。format3 原实现:
读完全部 backtrack/input/lookahead coverage 把 gid 收进 allGids 中间数组,任一不在
子集才置 triggerable=false,且每次调用分配 readCovGids 闭包 + allGids 数组。

优化:遇到不在子集的 gid 立即短路返回(triggerable=false 时 contextGids/refs 本就不
收集,后续 coverage 无需再读);去掉闭包与 allGids,triggerable 时 gid 直接 add 进
contextGids。逻辑完全等价(triggerable=false 时原代码也不 add contextGids)。

基准 29 用例 SSIM/ink/字节全无回归。初夏纯标点 2.34→1.86ms(-20%),FiraCode 3.7→3.4ms。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:45:08 +08:00
崮生(子虚)
7d62bce1df perf(gsub): readCoverageRemapped format2 range clamp 到 numGlyphs
承接 fda7ab8(format1 损坏 coverage 跳过)。插桩定位剩余 miss 热点:format2
(range) 仅 11 次 miss,但 439 个 range 的 end >= numGlyphs(1652),逐 gid 展开
浪费 ~320 万次 gidLookup 索引(全部 g>=numGlyphs 越界跳过),是 readCoverageRemapped
剩余 55% self time 的绝大头。

将每个 range 的 end clamp 到 numGlyphs-1:start..numGlyphs-1 段与原代码逐 gid 处理
完全相同,numGlyphs..end 段原代码全越界跳过、clamp 后省去空循环。整个 range 越界
(start>=numGlyphs) 时保留 origNonEmpty=true 语义(→ outOfSubset → null),与原代码
逐 gid 遍历全空结果完全等价。

基准 29 用例 SSIM/ink/字节全无回归,FiraCode fontSubset 6.8ms→3.75ms(累计较
优化前 12.2ms -69%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:39:12 +08:00
崮生(子虚)
fda7ab8902 perf(gsub): readCoverageRemapped 跳过损坏 coverage 的空循环
FiraCode subsetGSUB 的 readCoverageRemapped 占 70.5% self time。插桩定位:
77 次 format1 miss 中有 8 次 count 高达 15460(远超字体 glyph 总数 1652),
这些是 ChainContextSubst format3 指向错误偏移读到的垃圾 count,原代码循环
count 次全部 gidLookup[g] 越界返回 undefined 被跳过,浪费 107852 次迭代
(占 format1 miss 总工作量的 95%)。

count > numGlyphs 时跳过必然全空的循环,仅保留 origNonEmpty=count>0 语义
→ newGids 空 → outOfSubset=true → 返回 null,与原代码循环全空结果完全等价。
基准 29 用例 SSIM/ink/字节全无回归,FiraCode fontSubset 12.2ms→6.8ms(-45%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:34:10 +08:00
崮生(子虚)
d3d96d1735 perf(loca): subset 模式 Proxy 按需读,跳过全量 30907 项解析
subset 模式下 glyf.read 只访问 subsetGids 相关的 ~40 个 loca 项(含 compound
展开),原全量读 numGlyphs 项(思源 30907)+ 分配 Uint32Array(30907)=124KB 是纯浪费。
loca 表读取顺序在 glyf 之前,subsetGids 尚未构建,无法像 hmtx 那样按已知 gid 列表读。
改用 Proxy 拦截 glyf.read 的 loca[index] 访问,首次访问某 gid 时按需从 view 读并缓存。

实测 loca.read 从 profile 12.2% 降至 0%(思源标点),千字文 fontSubset -6.5%。
非 subset 路径保持原 TypedArray(全量访问下 Proxy trap 反慢)。基准 SSIM/ink/字节全无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:09:18 +08:00
崮生(子虚)
055425e852 perf(gsub): readCoverageRemapped format1 用 Uint16Array 批量读
coverage format1 的 gid 列表是连续 count 个大端 u16,原 DataView.getUint16
逐次调用有边界检查 + 大端组装开销。gid 数组 2 字节对齐时改用 Uint16Array
view 共享 buffer 读取 + 内联翻转 ((raw&0xff)<<8)|(raw>>8)(与 hmtx/loca 同思路),
未对齐回退 DataView。

readCoverageRemapped 是 FiraCode subsetGSUB 第一大热点(75.5%),miss 路径
sum 遍历 ~26000 gid。实测 FiraCode fontSubset 中位数 13.65→12.43ms(-8.9%),
5 轮稳定。基准 SSIM/ink/字节全无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:03:40 +08:00
崮生(子虚)
b4f8d7a522 perf(hmtx): subset 模式按需读 subsetGids 项,跳过全量 30888 项解析
subset 模式下 glyf.read(表顺序在 hmtx 之前)已构建 subsetGids,
resolveGlyf 也只访问 subsetGids 对应位置。原实现全量解析 numOfLongHorMetrics
项(思源 30888)对小子集是纯浪费。改为只读 subsetGids 需要的项,
其余位置保持 0(不被访问)。

实测 hmtx.read 0.21→0.032ms(6.5×),基准 SSIM/ink/字节全无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:46:42 +08:00
崮生(子虚)
6ea19550a1 perf(gsub-reachable): collectReachableGsubTargets 单 Set inSubset + 复用临时 Set
初夏明朝纯标点(最慢 CJK 用例 4ms)profile 显示 collectReachableGsubTargets 占 19.2% +
collectChainRefs 7.8%(合计 27%,超过 subsetGSUB 本身 6.4% 的 3 倍)。单次 1.22ms。

优化:
1. inSubset 闭包原为 seedGids.has(g) || reachable.has(g) 两次 Set 查询(热路径每 subtable
   多次调用)。改为 reachable 初始即含 seedGids,inSubset 只查 reachable 一个 Set。
   返回的 reachable 可能含 seed gid——调用方注入 extraSubsetGids 后 Font.create 去重,无害。
2. 每个 type6 subtable 原 new Set() × 2(refs + ctxGids),多轮迭代 × 51 lookup 累积 GC 压力。
   改为循环外复用 + clear。

实测初夏明朝 1.22→1.02ms(-16%),FiraCode 0.75→0.69ms(-8%)。
基准测试 29 用例 SSIM/ink/输出字节全部无回归(初夏纯标点 4.4→3.6ms)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:15:50 +08:00
崮生(子虚)
bbc3d8b88a perf(hmtx): LongHorMetric 段改 Uint16Array view 批量读+内联翻转
hmtx.read 原用 DataView.getUint16/getInt16 逐次调用读 LongHorMetric 段(每项 u16 advW + i16 lsb),
思源黑体 numOfLongHorMetrics=30888,占 fontSubset 约 8.7%(profile 仅次于 brotli/subsetGSUB)。

改用 Uint16Array view 共享 buffer 按下标读取后内联大端翻转(与 loca 优化同思路),实测快 2×
(198→98μs/call)。offset 需 2 字节对齐,未对齐回退 DataView。Last 段(lsb only)同样优化,
起始必然 2 字节对齐(前段是 4 字节倍数)。

基准测试 29 用例 SSIM/ink/输出字节全部无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:10:18 +08:00
崮生(子虚)
44f84e5659 perf(gsub-subset): serializeSingle/Multiple/Alternate/Ligature 改用 gidLookup 数组查找
serializeSingleSubst/MultipleSubst/AlternateSubst/LigatureSubst 原用 remapGid(origToNew Map)
对每个 coverage gid + 分量 gid 做哈希查询。subsetGSUB 入口已构建 gidLookup 数组
(下标=原gid,值=新gid/-1,比 Map.get 快 ~2×),但这四个函数未使用,仍传 origToNew Map。

serializeLigatureSubst 是 CJK ligature 子集化热点(50gid 子集占 subsetGSUB 65%,
每条 ligature 对全部分量密集 remapGid)。改为收 gidLookup 参数,remapGid(Map.get)
→ gidLookup[gid](数组索引,-1 即子集外)。

四个函数签名 origToNew: Map → gidLookup: GidLookup,serializeSubtable 调度相应更新。
ChainContext format1 的两处 remapGid(serializeChain/parseChainRuleFormat1or2)暂未改
(FiraCode 路径 serialize 占比小,readCoverageRemapped 是主瓶颈)。

验证:基准测试 29 用例 SSIM 全部无回归;4 用例 sha256 输出字节逐字节等价。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:45:39 +08:00
崮生(子虚)
51f969d3f0 perf(ot-bytes): OTWriter 改 Uint8Array 缓冲 + writeBytes 批量复制
OTWriter 原用 number[] + push 累积字节,每个 writeUint8/16 触发数字装箱与数组扩容;
gsub/gpos-subset 逐字节 writeUint8 循环复制 ScriptList/FeatureList/PairSet 字节块极慢。

重构 OTWriter 底层为 Uint8Array 容量缓冲 + size 指针:
- writeUint8/16 索引写入(无装箱),ensure 按 2× 扩容
- 新增 writeBytes(Uint8Array) 用 TypedArray.set 批量复制
- toUint8Array 返回 subarray 的拷贝(避免后续写入污染,subarray 语义曾有隐患)
gsub-subset/gpos-subset 的 5 处逐字节 writeUint8 复制改 writeBytes。

关键 Bug 修复:原 writeInt16 依赖 number[] 索引赋值到 length 位置【隐式扩展数组】,
Uint8Array 版需显式 ensure + 推进 size(否则 writeInt16 不增长 size,后续 write 覆盖,
GPOS 的 SinglePos 坐标值丢失——GPOS 输出从 1942B 错误缩到 1448B)。writeInt16At 仅覆盖
不扩展(pos 已在 reserveOffset16 的 ensure 保证范围内)。

验证:基准测试 29 用例 SSIM 全部无回归;GSUB/GPOS 独立输出 + fontSubset 完整输出
(思源千字文/标点/FiraCode/ttf)sha256 与重构前逐字节等价。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:39:04 +08:00
崮生(子虚)
d793e22d92 perf(loca): TypedArray 批量读+内联翻转替代 DataView 循环,loca 解析快 39%~46%
loca 偏移是大端,原 DataView.getUint16/32 逐次调用含边界检查开销。改用
Uint16/32Array 直接 view 共享 buffer + 内联字节翻转,微基准实测:
- format0(Uint16):157→84μs(-46%)
- format1(Uint32):152→93μs(-39%)
(思源 30907 条 loca)

返回 Uint32Array 替代普通 Array,glyf.js 仅按下标 loca[index] 只读访问,
与 TypedArray 兼容(已确认 loca 仅被 glyf.js:22 读取、ttfreader.js:216 置 null)。

对齐保护:Uint16Array 需 2 字节对齐、Uint32Array 需 4 字节对齐,loca offset
来自表目录不保证对齐。检测 srcByteOff 对齐情况,未对齐回退 DataView 路径。

验证:基准测试 29 用例 SSIM 全部无回归;4 用例(思源千字文/标点/FiraCode/ttf)
sha256 输出字节逐字节等价。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:25:39 +08:00
崮生(子虚)
6f96e2b1dd perf(post): subset 模式跳过 glyph name 解析降级 format 3,CJK 子集化提速 45%~50%
subset 模式下 post glyph name 对 web 字体渲染无用(浏览器靠 cmap→gid→glyf 渲染,
glyph name 仅用于 PDF 嵌入/编辑器)。原实现 subset 模式保存 pascalString 区
readBytes(思源 278KB)+ view 引用,resolveGlyf 再按需读 nameIndex + 算 name,
实测 post 段占 Font.create 的 69%(思源 56 glyph 子集 1.19ms)。

优化:subset 模式直接降级 post.format=3,跳过 nameIndex 解析、pascalString
readBytes、resolveGlyf 按需 name 读取。resolveGlyf 的 `2 === format` 条件不成立
整段跳过;write/size 走 format 3(32 字节,无 nameIndex/names)。

收益(CJK Font.create 是 fontSubset 主体):
- 思源千字文中段 3.057→1.673ms(-45%)
- 思源标点 2.705→1.361ms(-50%)
- FiraCode 22.2→21.0ms(-5.5%,瓶颈在 GSUB 不在此)
体积:FiraCode 8724→8704B(post 无 name 更紧凑)

验证:基准测试 29 用例 SSIM 全部无回归(千字文 0.9986、思源标点 0.9977、
初夏纯标点 0.9920、FiraCode 0.9923、otf-思源 0.9310 均不变)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:16:56 +08:00
崮生(子虚)
2c56d2cbb2 perf(font): 轻量 GSUB probe 跳过 glyf 解析,CJK 子集化提速 17%~56%
fontSubset 原先对 probe 阶段也调用完整 Font.create(subset 模式),
它会解析子集 glyph 的 glyf 轮廓(parseSimpleGlyf)。但 probe 只需要
GSUB(原始字节透传)和 cmap(codepoint→gid),根本不需要轮廓。
实测 CJK 字体(思源黑体/令东齐伋)两次 Font.create 占总耗时 36%~63%。

新增 gsub-probe.ts:直接从字体字节解析表目录,提取 GSUB 字节切片 +
cmap 的 codepoint→gid 查找(format4 BMP + format12 补充平面二分,
与 fonteditor-core readWindowsAllCodes 优先级一致),完全跳过 glyf。

三种路径(与原 Font.create probe 行为严格等价):
- otf / ttf 无 GSUB:直接判无 reachable(otf 的 fonteditor probe 本就
  origGSUB=undefined),连 probe Font.create 也跳过
- ttf 有 GSUB 且 cmap 含 fmt4/12:轻量 probe 算 reachable
- ttf 有 GSUB 但 cmap 无 fmt4/12(极罕见):回退 Font.create probe

验证:
- seedGids / GSUB字节 / reachable 与原 probe 逐项等价(FiraCode 87 target 等)
- 5 个代表字体 woff2 输出与优化前逐字节一致(cmp 通过)
- pnpx tsx 基准测试.test.ts 全部 SSIM 无回归:
  思源黑体-千字文中段 7.8→4.5ms(-42%)、千字文前段 11.9→8.9ms(-25%)、
  otf-思源 2.9→1.9ms(-34%)、霞鹜文楷-汉字标点 5.0→2.2ms(-56%)、
  FiraCode 18.5→17.2ms,SSIM 全部不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:41:16 +08:00
崮生(子虚)
2da35e4257 perf(gsub): emitCoverage 去掉冗余的 new Set 去重
emitCoverage 原用 Array.from(new Set(newGids)).sort() 去重+排序。
但所有调用方传入的 newGids 逻辑上保证无重复:
- readCoverageRemapped 按原 coverage(规范要求 gid 唯一)升序过滤,每个原 gid 映射唯一新 gid
- entries.map(e=>e.from) 的 from 是 entry 主键(唯一)
实测 FiraCode 单次子集化 327 次 emitCoverage 调用 0 次发现重复,
new Set 去重纯属 GC 开销(Set 构造+迭代+Array.from)。

改为 newGids.slice().sort(),消除 Set 分配。sort 保留(新 gid 随原 gid 非单调,需排序)。

验证:与优化前字节逐字节一致(cmp 通过,8724==8724),
pnpx tsx 基准测试.test.ts 全部 SSIM 无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:16:45 +08:00
崮生(子虚)
5da4cd5b06 perf(gsub): fmt3 ChainContext 预检跳过 + 修复 covCache 共享语义污染
预检优化(type1-4 + fmt1 + fmt3 全覆盖):
- 重构 isPrimaryCoverageOutOfSubset → isSubtableSkipableByCoverage
  + 抽出 coverageAllOutOfSubset 辅助函数(内联遍历,不分配数组、不碰缓存)
- 新增 ChainContext format3 预检:遍历 back/input/look 三组 coverage,
  任一组「原非空且全子集外」即判定 subtable 失效,跳过深度解析
- FiraCode 403 lookup 中 ~330 个可预检跳过,子集化 23ms→16ms(-31%)

covCache 共享语义污染修复(fmt3 预检能安全启用的前提):
- CoverageCacheEntry 的 gids(原始 gid,fmt1 按 index 配对)与
  remapped(newGids,fmt3 序列化)两种语义原混写同一字段:
  旧 readCoverageRemapped 把 newGids 写进 gids,污染后续 fmt1 的
  readCoverageGids(拿到 newGids 当原始 gid 二次重映射,连字规则错位)
- 修复:readCoverageRemapped 只写 remapped,gids 留单例占位 EMPTY_GIDS;
  readCoverageGids 见占位即重新计算并 mergeGidsEntry(保留已有 remapped)
- 修复后 fmt3 预检开关下字节完全等价(8724==8724),SSIM 全程无回归

验证:pnpx tsx 基准测试.test.ts 全部 SSIM 无回归
(FiraCode=0.9923, otf-思源黑体=0.9310, 初夏明朝-纯标点=0.9920 不变)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:05:46 +08:00
崮生(子虚)
25da274c3a perf(gsub): subsetGSUB 主 coverage 预检跳过空 lookup,FiraCode 33ms→22ms
FiraCode 含 403 个 lookup,子集仅命中少数字形,多数 lookup 主 coverage 引用
的 gid 全部不在子集内——深度序列化后必然得到空 entries、回退 writeEmptySubtable。
预检在深度解析前用 gidLookup 内联判定主 coverage 是否全子集外,命中即跳过
(type1-4 + type6-format1,共 ~216 个空 lookup),消除其 entries 构建/rule 解析开销。

关键正确性:
- format2(class 驱动)/format3(多 coverage)不预检——主 coverage 非充分条件,
  误判全空会丢连字(实测 FiraCode calt format2,SSIM 0.9923→0.9368)
- 预检不经 covCache——该缓存被 readCoverageRemapped(存 newGids)与
  readCoverageGids(format1 期望原始 gid)共享,语义不一致,预检写入会污染
  format1 深度解析(newGids 被当原始 gid 二次重映射,连字规则错位)
- 输出字节与预检关闭时完全等价(已 dump 对比验证),SSIM 全部无回归

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:45:43 +08:00
崮生(子虚)
771f4fc6de perf(gsub): GidLookup 用 number[] + format1 预分配,FiraCode 30ms→23ms
- GidLookup 从 Int32Array 改为 number[]:实测 V8 下普通数组索引访问比
  TypedArray 快约 2×(coverage 过滤高频查询)
- readCoverageRemapped format1 预分配 newGids(count) 索引写入后截断,
  避免 push 动态扩容

SSIM 0.9923 保持,输出 8708B 稳定。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:38:05 +08:00