项目中使用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的小。
将一个mask绘制到界面主要有两个步骤,这两个步骤都比较耗时,第一步是转换,将mask转成imageData,这一步是纯计算的,会放在worker里面,第二步是绘制,以下只关注转换这一步,第二步暂不关注(虽然它也很慢,还存在可优化的空间)。
由于worker是异步的,转化分为三个小步骤:
与worker交互,数据需要序列化、反序列化,当对象很大时,是很耗性能的,如下的例子就可以充分说明,不管mask多大,postMessage to worker这一步会花不少时间,实际上mask转行是很快(1毫秒左右),但来回的传输就很慢了
重构为Wasm的第一版版本,我选择跟worker类似的方案,只是用rust实现而已,也是分三个小步骤:
结论:实际测试下来,最后一步将image_data序列化很耗时,总体时间甚至比worker的还慢,所以此方案废弃了。
数据序列化和反序列化通常很耗时,那有没有方案绕过它?答案就是共享内存,这也是wasm推荐的做法之一,与wasm交互时,数据只有一份且存在wasm的memory对象中,浏览器直接读取wasm中内存数据并渲染,无需额外空间,三个小步骤
说明:上面的测试数据就是基于这个方案实现的,实际上还有优化空间,内存还可以优化一下,时间关系就没做了。
上面的方案还没完全利用wasm的优势,mask转换是可以并发的,那么能否用并发去转换mask呢?
wasm原生还不支持异步(听说有计划支持),想在浏览器跑多线程,目前只有worke能做到了,我们可以跑多个woker,在每个worker里面再去调用wasm,这样在mask数量很多的情况下,利用多线程,性能又能提升不少
说明:项目还未实现,github上有一个使用wasm + woker做多线程的例子,可以打开去体验下:https://github.com/jsdw/wasm-fractal
当前是把所有mask转成imageData之后再统一渲染(draw)的,当mask很多、很大的时候,有可能会因为内存占用过多而导致浏览器卡死,比较好的解决方案是分片渲染,也即边转换、边绘制,这样用户体验也会好很多(虽然总体时间并不会减少)。重构后的wasm方案,支持分片渲染会更灵活,因为它是每一个mask单独转换,可以灵活控制,内存及时回收,浏览器不容易崩掉(当前的worker是批量转换的)
除了mask这一步可以用wasm实现之外,绘制是否也可以呢,我认为只要涉及到大量像素点操作的,应该都可有用wasm,当然啦,这一切还要考虑开发成本。
留言与评论(共有 0 条评论) “” |