从consolidation 的时候 blockData 被多次深拷贝带来的卡顿的问题分析到了权威数据源的多次全量拷贝带来的设计问题,接下来是深入的分析。
代码基线:
1ed3544
Trace 分析结论:consolidation 触发的持久化深拷贝是卡顿的主因
通过 Chrome Trace 分析,定位到一次删除方块后触发 consolidation 的完整卡顿链路:
单帧卡顿 1422ms
一个动画帧耗时 1422ms(正常 ≤16ms),FunctionCall 事件分解如下:
| 耗时 |
位置 |
说明 |
| 1422ms |
Game.js:425 |
游戏主循环帧(总) |
| 392ms |
IndexedDBUtils.js:58 request.onsuccess |
IndexedDB 事务提交 + 内部 structuredClone |
| 376ms |
IndexedDBUtils.js:58 request.onsuccess |
同上 |
| 358ms |
WorkerRpcClient.js:15 worker.onmessage |
Worker 返回 consolidation 结果后主线程处理 |
| 358ms |
PersistenceWorker.js:343 self.onmessage |
PersistenceWorker 处理 saveRegionRecord |
| 172ms |
PersistenceWorker.js:343 self.onmessage |
同上 |
根因:blockData 被多次 structuredClone
在 consolidation → 持久化链路中,同一份 blockData 被克隆了 3~4 次:
主线程 WorldRuntime._flushDirtyChunks()
│
├─ ① _serializeBlockData() ← Map → 普通对象转换
│
└─ postMessage('applyRegionPatch')
│
└─ ② 浏览器内部 structuredClone ← postMessage 跨 Worker 传递
│
PersistenceWorker.self.onmessage (358ms)
│
├─ ③ structuredClone(region) ← PersistenceWorker.js:235 显式深克隆
│ 整份 region 对象,含所有 chunk 的 blockData
│
└─ saveRegionRecord → store.put({ data: record })
│
└─ ④ IndexedDB 内部 structuredClone ← 规范要求存储隔离
为什么明明是内存存储还这么慢?
不是磁盘 I/O,是 structuredClone 的 CPU 开销。structuredClone 对大型嵌套对象是 O(n) 的遍历和复制操作。一个 region 包含多个 chunk,每个 chunk 的 blockData 有几百到上千个坐标映射,clone 时每个 key-value 都要递归处理。
IndexedDB 规范要求在存储前对数据进行结构化克隆以保障数据隔离,所以即使你把数据存在内存里(无磁盘延迟),store.put(largeObject) 仍然会触发一次完整的 structuredClone。
优化方向
- 去掉
PersistenceWorker.js:235 的显式 structuredClone(P0)—— IndexedDB 内部本身就会 clone,显式 clone 是纯浪费。改为原地修改 region 缓存,写入失败再从 IndexedDB 回读恢复
- 将
_flushDirtyChunks 的刷写与 consolidation 错峰(P0)—— consolidation 完成后延迟再触发持久化,避免两者抢 CPU
- 考虑使用 Transferable 传递 blockData(P1)—— postMessage 时用 transfer list 转移 ArrayBuffer 所有权,避免跨线程 clone
- 将大 region 粒度拆分(P2)—— 减少单次
store.put 的数据量
为此我想将权威数据源统一到 blockData 身上,写了一个方案:
https://gist.github.com/jayli/da68916e68272032abe7cc7e79b5a146
还没有详细review这个方案的合理性
从consolidation 的时候 blockData 被多次深拷贝带来的卡顿的问题分析到了权威数据源的多次全量拷贝带来的设计问题,接下来是深入的分析。
代码基线:
1ed3544
Trace 分析结论:consolidation 触发的持久化深拷贝是卡顿的主因
通过 Chrome Trace 分析,定位到一次删除方块后触发 consolidation 的完整卡顿链路:
单帧卡顿 1422ms
一个动画帧耗时 1422ms(正常 ≤16ms),FunctionCall 事件分解如下:
Game.js:425IndexedDBUtils.js:58request.onsuccessIndexedDBUtils.js:58request.onsuccessWorkerRpcClient.js:15worker.onmessagePersistenceWorker.js:343self.onmessagePersistenceWorker.js:343self.onmessage根因:blockData 被多次 structuredClone
在 consolidation → 持久化链路中,同一份 blockData 被克隆了 3~4 次:
为什么明明是内存存储还这么慢?
不是磁盘 I/O,是 structuredClone 的 CPU 开销。structuredClone 对大型嵌套对象是 O(n) 的遍历和复制操作。一个 region 包含多个 chunk,每个 chunk 的
blockData有几百到上千个坐标映射,clone 时每个 key-value 都要递归处理。IndexedDB 规范要求在存储前对数据进行结构化克隆以保障数据隔离,所以即使你把数据存在内存里(无磁盘延迟),
store.put(largeObject)仍然会触发一次完整的 structuredClone。优化方向
PersistenceWorker.js:235的显式structuredClone(P0)—— IndexedDB 内部本身就会 clone,显式 clone 是纯浪费。改为原地修改 region 缓存,写入失败再从 IndexedDB 回读恢复_flushDirtyChunks的刷写与 consolidation 错峰(P0)—— consolidation 完成后延迟再触发持久化,避免两者抢 CPUstore.put的数据量为此我想将权威数据源统一到 blockData 身上,写了一个方案:
https://gist.github.com/jayli/da68916e68272032abe7cc7e79b5a146
还没有详细review这个方案的合理性