我們都知道,如果大量的數據計算直接放在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.在//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,那么使用方案五是目前我認為比較好的實踐方案,方案六可能是未來的最優解