Webassembly 实战

背景

项目中使用Canvas绘图,有一个笔刷功能,后端会给前端mask编码的数据,前端需要转成ImageData,才能绘制到Canvas上,这个转换相当耗时,当前是放在worker里计算,然后再返回ImageData渲染,当mask很多、很大的情况时,还是会存在性能问题,一方面内存占用过大、一方面是执行时间过长。有没有优化的空间?


最近看了一本关于Rust的书籍,想看一下基于Rust的Webassembly性能咋样,于是首先想到了mask的功能,先在这个功能实验一下,如果还不错,可以推广到其它场景,Canvas的很多底层操作都比较耗性能,如果能用wasm,甚至可以媲美原生客户端。另外将来产品也有可能推出客户端版本,也许基于Tauri的客户端版本也是一种选择,可以做一些技术储备。

数据对比

经过痛苦的重构(Rust的语法真的太难了...),终于把mask转换的代码重构为wasm,并接入Vimo,看看一下数据如何:

描述

mask示例

worker性能

wasm性能

说明

mask数量少、尺寸小


平均:30ms


request耗时很高


平均<1ms

mask尺寸很小的情况下,wasm的性能远远超过worker


mask数量多、尺寸大


三张图片,分别是:8155ms、2215ms、5309ms

三张图片,分别是:1606ms、373ms、783ms

mask数量多,尺寸也较大的情况下,wasm的性能是worker的4倍以上

mask尺寸大


同一张图片测试两次,平均15000ms

同一张图片测试两次,平均2250ms

mask尺寸超大的情况,wasm性能是worker的6倍


初步结论

转换性能:mask的性能在各种场景下都优于worker,性能至少提升4倍以上,当mask很小时,性能是worker的30倍。

内存占用:内存占用不太好测试,用浏览器的Performance简单测试了一下,wasm的内存占用也比worker的小。


技术调研

Worker 方案

将一个mask绘制到界面主要有两个步骤,这两个步骤都比较耗时,第一步是转换,将mask转成imageData,这一步是纯计算的,会放在worker里面,第二步是绘制,以下只关注转换这一步,第二步暂不关注(虽然它也很慢,还存在可优化的空间)。


由于worker是异步的,转化分为三个小步骤:

  1. postMessage to worker ,这一步将需要转换的mask传给worker
  2. Generate ImageData,这一步将mask转成ImageData,像素点操作,性能问题主要出现在这一步骤
  3. postMessage to brower,这一步将转换好的ImageData传回给浏览器,然后就可以绘制啦!

与worker交互,数据需要序列化、反序列化,当对象很大时,是很耗性能的,如下的例子就可以充分说明,不管mask多大,postMessage to worker这一步会花不少时间,实际上mask转行是很快(1毫秒左右),但来回的传输就很慢了

Webassembly 模拟worker方案

重构为Wasm的第一版版本,我选择跟worker类似的方案,只是用rust实现而已,也是分三个小步骤:

  1. mask_data to JsValue,与wasm交互,消息同样需要序列化
  2. Generate ImageData,mask转换,跟worker逻辑一样
  3. image_data to JsValue,将image_data序列化后给到浏览器

结论:实际测试下来,最后一步将image_data序列化很耗时,总体时间甚至比worker的还慢,所以此方案废弃了。

Webassembly 共享内存方案

数据序列化和反序列化通常很耗时,那有没有方案绕过它?答案就是共享内存,这也是wasm推荐的做法之一,与wasm交互时,数据只有一份且存在wasm的memory对象中,浏览器直接读取wasm中内存数据并渲染,无需额外空间,三个小步骤


  1. Write mask to share memory,将需要转换的数据写入wasm内存,不再通过序列化传给wasm了
  2. Generate ImageData,从内存读取mask数据并转成ImageData,并将ImageData写入share memory,不再通过序列化传给浏览器
  3. Read ImageData from share memory,浏览器直接读取wasm内存,并绘制,无需再构造对象

说明:上面的测试数据就是基于这个方案实现的,实际上还有优化空间,内存还可以优化一下,时间关系就没做了。

Webassembly 多线程方案

上面的方案还没完全利用wasm的优势,mask转换是可以并发的,那么能否用并发去转换mask呢?


wasm原生还不支持异步(听说有计划支持),想在浏览器跑多线程,目前只有worke能做到了,我们可以跑多个woker,在每个worker里面再去调用wasm,这样在mask数量很多的情况下,利用多线程,性能又能提升不少

说明:项目还未实现,github上有一个使用wasm + woker做多线程的例子,可以打开去体验下:https://github.com/jsdw/wasm-fractal


One More Thing

当前是把所有mask转成imageData之后再统一渲染(draw)的,当mask很多、很大的时候,有可能会因为内存占用过多而导致浏览器卡死,比较好的解决方案是分片渲染,也即边转换、边绘制,这样用户体验也会好很多(虽然总体时间并不会减少)。重构后的wasm方案,支持分片渲染会更灵活,因为它是每一个mask单独转换,可以灵活控制,内存及时回收,浏览器不容易崩掉(当前的worker是批量转换的)


除了mask这一步可以用wasm实现之外,绘制是否也可以呢,我认为只要涉及到大量像素点操作的,应该都可有用wasm,当然啦,这一切还要考虑开发成本。

参考

  1. Setup - Rust and WebAssembly
  2. 入门 Rust 开发 WebAssembly
  3. rustwasm.github.io
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章