怡氧文件打开速度优化:一次步步深入的探索

作为一名关注用户体验的工程师,我一直对“怡氧”打开文件的速度有所关注。提升打开速度,改善用户体验,是我们一直在努力的方向。本文将分享我们团队在优化文件打开速度过程中的一些探索和尝试。

问题分析:性能瓶颈在哪里?

经过一番分析,我们发现文件打开的耗时主要集中在以下几个阶段:

  1. **JS SDK 加载:** 庞大的 `sdkjs` 文件在加载和解析时会消耗一定时间。
  2. **字体文件加载:** 渲染文字需要加载多个字体文件,部分字体文件体积较大,加载需要时间。
  3. **X2T 解析:** 调用 `x2t` 将文件解析为 `Editor.bin`,再将文件加载到 JS 环境中。

其中,`x2t` 是一个关键环节,涉及到文件格式转换,是文档打开流程中不可或缺的一步。

方案探索:各种尝试与思考

针对以上瓶颈,我们团队集思广益,尝试了一些优化方案,其中一些方案也带来了一些新的思考。

  • **字体文件缓存 + Memmap?**我们曾考虑过缓存字体文件,然后使用 `memmap` 直接加载。`memmap` 可以将文件映射到内存,减少重复读取。为了实现这个方案,我们还研究了 Electron 的 `nodeIntegration` API,尝试在渲染线程调用 Node.js 环境。
    https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions nodeIntegration
    测试用的 npm 库 https://www.npmjs.com/package/mmap-io 然而,在实际尝试中,我们发现 `memmap` 存在一些问题:Page Fault Remap 时可能会阻塞渲染线程,导致界面出现卡顿。

    考虑到 Electron 的版本升级,以及 V8 引擎对外部内存访问的限制,我们最终放弃了这个方案。
  • **V8 Heap Snapshot?**我们也曾考虑过利用 V8 Heap Snapshot 加速加载。但很快我们意识到,Heap Snapshot 只是内存快照,本质上是数据文件,并不能直接用于代码执行。JavaScript 代码仍然需要经过解析和编译才能执行,这个过程无法省略。
    Heap Snapshot 不是可执行代码: Heap Snapshot 是 V8 引擎内存状态的快照,它本质上是一个数据文件,包含了对象、类型、引用关系等信息。它不是可以直接执行的 JavaScript 代码或者编译后的机器码。编译是必须的: JavaScript 引擎必须先将 JavaScript 代码解析成抽象语法树 (AST),然后再将 AST 编译成机器码才能执行。 这个编译过程是不可避免的。 Heap Snapshot 只能在代码执行后生成。
    因此,这个方案也未能达到预期的效果。

新思路:WASM 的应用

在探索过程中,我们逐渐将目光转向 WASM:**将 `x2t` 运行在 WASM 中!**

这样做具有以下潜在优势:

  1. **提高文件解析效率:** WASM 具有接近原生代码的执行效率,可以提升 `x2t` 的解析速度。
  2. **增强安全性:** WASM 运行在沙箱环境中,可以降低恶意文件对后端造成影响的风险。

令人欣喜的是,我们发现 GitHub 上已经有开发者完成了将 `x2t` 编译为 WASM 的工作:https://github.com/cryptpad/onlyoffice-x2t-wasm

借助 WASM,我们可以将原来通过 Node.js `child_process.spawn` 调用可执行程序的过程转移到渲染线程。并且,我们还可以考虑直接通过内存将 `Editor.bin` 内容传递给渲染进程,减少文件读写的步骤。

技术调研:JS 与 WASM 的数据交互

确定 WASM 的方向后,我们需要解决一个重要问题:如何在 JS 和 WASM 之间高效地交换数据?

我们向 AI 助手咨询了 JS 和 WASM 数据交换的多种方式,得到了以下方案:

// 1. Direct numeric parameter passing (fastest, but limited to numbers)
// 2. Linear Memory Access (high performance for bulk data)
// 3. SharedArrayBuffer (zero-copy, but requires cross-origin isolation)
// 4. Structured Data Exchange (for complex objects)
// 5. Transferable Objects (e.g., ArrayBuffer)
// 6. WebAssembly.Table for function references
// 7. Streaming SIMD Data
// 8. Asyncify for Async Operations
// 9. Web Workers with SharedArrayBuffer
// 10. Atomics for Synchronization

为了找到适合我们的方案,我们进行了性能测试。

性能测试:数据驱动决策

我们利用工具辅助,编写了一系列测试程序,针对浏览器相关 API (WASM Shared Memory, Atomics, SharedArrayBuffer) 进行了较为详细的性能测试。

测试结果显示:

  • 在 JS 渲染线程和 WebWorker 中,性能差异不明显,但大块数据的处理可能会阻塞 UI。
  • WASM 需要使用 `WebAssembly.Memory` 实例化对象,`SharedArrayBuffer` 对象不能直接传递给 WASM 使用。
  • 不使用 `WebAssembly.Memory` 实例化 Memory 对象,而是直接通过 WASM 实例化的对象访问 WASM 中的内存是可行的。
  • `WebAssembly.Memory` 存在内存大小限制。
  • JS 和 WASM 两个交替操作相同位置的内存时,可能会出现问题,需要用 Atomics API 进行同步。但在某些情况下,即便使用了 Atomics API,WASM 读写数据仍然可能出现问题。

我们还对 WASM 读写文件的性能进行了测试和优化,并取得了一定的进展:

数据量优化前 (ms)优化后 (ms)
64M11805.45134.99
256M46676.62576.21
512M94539.741264.35
1G188446.262676.79

此外,我们还测试了 `Transferable Objects` 的性能,结果表明,在 WebWorker 中使用 `Transferable Objects` 传递大文件,能够带来明显的性能提升。

场景耗时 (ms)
IPC1788
WebWorker357.599
WebWorkerTransfer145.4

在数据传递方面,我们对比了内存共享和 Transferable Objects 两种方案。 内存共享需要复杂的同步机制,而 Transferable Objects 则提供了一种“所有权转移”的零拷贝传递方式。 考虑到 WASM X2T 的场景是将处理结果传递给主线程,而不是共享数据,我们最终选择了 Transferable Objects 方案。 这样可以避免数据拷贝,提高性能,并且简化并发编程的复杂性。详细说明请参考这篇文章

方案选择:WebWorker + WASM X2T

综合考虑测试结果,我们选择了以下方案:

  1. 在渲染进程中通过 WebWorker 执行 WASM X2T。
  2. 利用 Electron 的 `nodeIntegrationInWorker` 在 Worker 中使用 Node.js 的 `fs` 模块处理文件。
  3. 使用 Transferable Objects 在主线程和 WebWorker 之间传递数据。

字体文件处理:自定义协议

值得一提的是,目前版本的“怡氧”已经在使用 WASM 处理字体源文件了。加载方式是通过自定义协议,可以实现类似直接磁盘 IO 的效果。

总结与展望

目前,我们已经完成了文件打开速度优化方案的设计和技术验证。方案能否达到预期效果,还需要在实际开发完成后进行进一步测试。

这次优化过程充满了挑战,我们也在不断探索和学习。在后续的开发中,我们也会继续关注性能,持续优化用户体验。

如果您在文件处理优化方面有经验,欢迎在评论区分享您的见解!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注