我们都知道,如果大量的数据计算直接放在js主线程去执行,那么肯定会造成页面假死的状态,这是我们不能容忍的,并且在多可视化图形并行渲染的过程中,甚至导致可视化的动画完全丢失,体验很差。
一、数据分片
最简单的方案,就是把这大量的数据进行分片异步处理,处理完一段数据之后,释放主线程,让ui渲染进程有机会能够执行,在回头继续处理第二段数据,直到所有分片执行完成,更新最终UI侧数据
二、将数据直接发送给web worker执行
// 以下为伪代码,了解思想即可
/*
* main.js
*前端fetch到数据之后,直接发送给worker进行计算
*/
const bigData = [] // 500M的数据
const worker = new Worker("./worker.js");
worker.postMessage(bigData)
- 如果直接发送这份数据给到worker,postMessage会对这份数据做一次拷贝,那么浏览器就存储了1G数据,
在拷贝的过程中还会对数据进行序列化,在worker中,还需要对数据反序列化,无疑增加了很多成本
/*
* worker.js
*/
self.onmessage = funcion({ data }){
// calcData 进行复杂的计算逻辑
}
/*
*/
三、web worker优化方案
有没有办法,能够把发送给worker的这份数据,不要拷贝,而是指针(地址)的方式,worker直接进行计算,然后返回给我们一个标记,我们直接还是调用之前的那份数据呢?是有办法的
worker.postMessage(aMessage, transferList)
这个transferList是一个可选数组,用于传递所有权,什么意思呢,就是发送给worker之前,主线程是可以使用当前这份数据的,当一旦发送给worker之后,就失去了对这份数据的控制权,也就无法进行遍历或者进行分片等操作了,只有等worker处理好之后,在将这份数据发送回来,如果worker异常或者没有发送回来,那么这份数据就将会丢失掉。
好吧,那我就是想用,做好容灾就行。还有一个局限性,我们传递的这份数据是要实现Transferable接口的,目前前端实现这个接口的数据结构有三个,ArrayBuffer,MessagePort,ImageBitmap,那我们就拿ArrayBuffer来举例吧(只对它有所了解)
那看来,如果我们要使用ArrayBuffer,就要对数据进行约束了,因为二进制数组存放的都是二进制数据,并且支持的类型有以下几种
这里插入图片
main.js
const bigData = [] // 500M的数据
const worker = new Worker("./worker.js");
/*
把原始数据结构中我们所需要的数据提取出来
*/
worker.postMessage(bigData)
/*
* worker.js
*/
self.onmessage = function({ data }){
const { buffer } = data;
// 如果要操作这份数据,在创建一个view视图,具体typedArray的操作api,可以查看MDN文档
// 创建视图,简单来说,就是去观察二进制数组buffer中的数据,并不是实例化拷贝一份数据出来,buffer中的数据一个黑盒,不能直接进行操作,要先定义视图并且赋予期数据类型,才可进行操作
const view = new Int32Array(buffer)
// 对view进行复杂操作,返回给主线程进行读取
self.postMessage(view,[view.buffer]);
}
四、共享内存sharedArrayBuffer
上面的方案,我们发现一个问题,就是数据控制权的问题,以及虽然没有发生数据拷贝(相当于剪切),耗用的内存是不变的,更进一步来说,能不能直接把内存地址给到worker,woker直接去操作这份数据呢?
const worker = new Worker("./worker.js");
// 实例化共享内存
const buffer = new SharedArrayBuffer(arr.legnth * Int32Array.BYTES_PER_ELEMENT);
worker.postMessage({ buffer });
worker.onmessage = function () {
const view = new Int32Array(buffer);
console.log(buffer, view);
}
/*
注意:因为SharedArrayBuffer存在安全漏洞的原因,在2018年被叫停了,目前只能在chrome和Mozilla中开启一定的安全策略下使用,可以先了解,未来可期
开启SharedArrayBuffer的两种方法
1.开启 Cross-Origin-Opener-Policy: same-origin
2.在命令窗口,启动chrome --enable-features=SharedArrayBuffer,仅用于调试
3.在https://developer.chrome.com/origintrials/#/registration,注册,返回一个临时token,可用于调试
*/
这样就已经实现了在worker直接分发数据零拷贝,每个不同weorker直接将接受到的数据,进行处理,修改buffer,处理完成之后,告诉主进程已完成done,主进程进行ui更新渲染即可,不过目前SharedArrayBuffe不能用于生产环境。
五、webassembly方案
整体实现思路:
1.调用webAssembly的方法,在webAssembly内存中分配空间,返回指针
2.JS端在webAssembly的memory内存中申请一段arrayBuffer,根据指针位置和数据量建立view视图,并且把数据写入arrayBuffer中
3.调用webAssembly方法完成计算(将步骤一中申请的内存指针传入,再返回计算完成的批量结果的指针位置和大小
4.JS端在webAssembly的memory arraybuffer上,按指针位置和数据量建立view,把数据读出,进行UI渲染
以下是代码的具体实现
#include <emscripten.h>
#include <iostream>
#include <algorithm>
#include <time.h>
using namespace std;
#ifdef __cplusplus
extern "C"
{ // C++ compiler 不会优化函数名,导出到js中,能够按照原始名称导出
#endif
EMSCRIPTEN_KEEPALIVE int main()
{
return 100;
}
EMSCRIPTEN_KEEPALIVE void sortArr(int *ptr)
{
sort(ptr, ptr + 9);
}
EMSCRIPTEN_KEEPALIVE void randomArr(int *ptr)
{
srand((unsigned)time(NULL));
for (int i = 0; i < 9; i++)
{
ptr[i] = rand();
}
}
// 这里进行内存申请,其实调用的是wasm memory的线性内存申请
EMSCRIPTEN_KEEPALIVE int *getArrPointer(int capacity)
{
int *prt = (int *)malloc(sizeof(int) * capacity);
cout << "c++申请的内存地址为:" << prt << endl;
return prt;
}
#ifdef __cplusplus
}
#endif
使用emcc编译工具进行编译,得到一个js胶水文件,和一个.wasm文件
// 加载胶水文件,会得到一个Module对象,具体api细节,可网上查阅
const self = this;
Module.onRuntimeInitialized = function () { // 初始化之后
const bigData = [];
// 申请内存,返回指针
const dataPointer = Module._getArrPointer(bigData.length);
// 从memory中读取数据块,注意,这里一定要写偏移量dataPointer
const dataview = new Int32Array(Module['asm']['memory'].buffer, dataPointer, bigData.length)
for (let i = 0; i < bigData.length; i++) {
dataview[i] = bigData[i];
}
// c++业务逻辑处理
Module._sortArr(dataPointer);
// 处理完成新建一个view读取,返回给UI进行渲染
}
六、其他调研以及一些想法
1.用sharedArrayBuffer初始化个wasm实例,用worker进行初始化多个wasm实例,共享一片内存
#include <stdlib.h>
#include <emscripten.h>
#ifdef __cplusplus
extern "C"
{ // So that the C++ compiler does not rename the functions below
#endif
EMSCRIPTEN_KEEPALIVE int *getArrPointer(int capacity)
{
int *prt = (int *)malloc(sizeof(int) * capacity);
prt[0] = 10;
return prt;
}
#ifdef __cplusplus
}
#endif
实例化wasm
const memory =
new WebAssembly.Memory({
initial: 80,
maximum: 80,
shared: true
});
// 编译阶段,采用emcc change1.cpp -O3 -o change1.wasm --no-entry -Wl,--import-memory,
* @param {*} e
* 1、加载wasm文件
* 2、使用主进程传入的memory对象,进行初始化wasm,调用c++函数,申请内存,返回一个指针
* 3、
*/
self.onmessage = function ({ data }) {
const { memory } = data;
// 使用这个meory进行wasm的初始化,共享这片内存
console.log("worker 执行了", memory)
fetch("change1.wasm").then(response =>
response.arrayBuffer()
).then(bytes => {
return WebAssembly.instantiate(bytes, {
env: {
'__table_base': 0,
'memory': memory,
'__memory_base': 1024,
'STACKTOP': 0,
'STACK_MAX': memory.buffer.byteLength,
}
})
}
).then(({ instance }) => {
console.log("实例化后的wasm对象", instance);
console.log("传入对象的memory", memory)
// const dataPointer = instance.exports.getArrPointer(10);
// console.log("worker1中的申请的内存为:", dataPointer);
// self.postMessage(dataPointer)
});
}
上述方案其实比较完美,不过遇到了一个问题:
LinkError: WebAssembly.instantiate(): mismatch in shared state of memory, declared = 0, imported = 1
好像提示是使用了两个不同内存实例,目前还比较困惑,没找到合理的解释,有进展后续更新~
以上是做的一些调研,下面总结一下
1、如果非必要的情况,不建议使用wasm这种解决方案,可以简单用web worker实现,市面上很多都是一些简单的理论,实践起来坑比较多,成本也很高。
2、如果使用webassembly,那么使用方案五是目前我认为比较好的实践方案,方案六可能是未来的最优解