获取 WebAssembly 实例化流的进度

Get WebAssembly instantiateStreaming progress

WebAssembly.instantiateStreaming is the fastest 下载和实例化 .wasm 模块的方法,但是对于大型 .wasm 文件,它仍然需要很长时间。在这种情况下,简单地显示微调器并不能提供足够的用户反馈。

有没有办法使用 WebAssembly.instantiateStreaming api 并获得某种形式的进度事件,以便可以向用户显示 eta?理想情况下,我希望能够显示百分比进度条/估计剩余时间指示器,以便用户知道他们需要等待多长时间。

建立答案 here

要获得 WebAssembly.instantiateStreaming / WebAssembly.compileStreaming 的进度,请使用实现其自己的控制器的自定义 ReadableStream 创建一个新的 Fetch Response。

示例:

// Get your normal fetch response
var response = await fetch('https://www.example.com/example.wasm'); 

// Note - If you are compressing your .wasm file the Content-Length will be incorrect
// One workaround is to use a custom http header to manually specify the uncompressed size 
var contentLength = response.headers.get('Content-Length');

var total = parseInt(contentLength, 10);
var loaded = 0;

function progressHandler(bytesLoaded, totalBytes)
{
    // Do what you want with this info...
}

var res = new Response(new ReadableStream({
        async start(controller) {
            var reader = response.body.getReader();
            for (;;) {
                var {done, value} = await reader.read();

                if (done)
                {
                    progressHandler(total, total)
                    break
                }

                loaded += value.byteLength;
                progressHandler(loaded, total)
                controller.enqueue(value);
            }
            controller.close();
        },
    }, {
        "status" : response.status,
        "statusText" : response.statusText
    }));

// Make sure to copy the headers!
// Wasm is very picky with it's headers and it will fail to compile if they are not
// specified correctly.
for (var pair of response.headers.entries()) {
    res.headers.set(pair[0], pair[1]);
}

// The response (res) can now be passed to any of the streaming methods as normal
var promise = WebAssembly.instantiateStreaming(res)

基于各种其他 SO 答案,这就是我的最终结果。

我的解决方案还为 Firefox 提供了不错的回退,它还没有适当的流支持。我选择回退到一个很好的旧 XHR 和 WebAssembly.Instantiate 那里,因为我真的很想显示一个加载栏,即使这意味着在 FF 上启动稍微慢一些。

  async function fetchWithProgress(path, progress) {
    const response = await fetch(path);
    // May be incorrect if compressed
    const contentLength = response.headers.get("Content-Length");
    const total = parseInt(contentLength, 10);

    let bytesLoaded = 0;
    const ts = new TransformStream({
      transform (chunk, ctrl) {
        bytesLoaded += chunk.byteLength;
        progress(bytesLoaded / total);
        ctrl.enqueue(chunk)
      }
    });

    return new Response(response.body.pipeThrough(ts), response);
  }

  async function initWasmWithProgress(wasmFile, importObject, progress) {
    if (typeof TransformStream === "function" && ReadableStream.prototype.pipeThrough) {
      let done = false;
      const response = await fetchWithProgress(wasmFile, function() {
        if (!done) {
          progress.apply(null, arguments);
        }
      });
      await WebAssembly.InstantiateStreaming(response, importObject);
      done = true;
      progress(1);
    } else {
      // xhr fallback, this is slower and doesn't use WebAssembly.InstantiateStreaming,
      // but it's only happening on Firefox, and we can probably live with the game
      // starting slightly slower there...
      const xhr = new XMLHttpRequest();
      await new Promise(function(resolve, reject) {
        xhr.open("GET", wasmFile);
        xhr.responseType = "arraybuffer";
        xhr.onload = resolve;
        xhr.onerror = reject;
        xhr.onprogress = e => progress(e.loaded / e.total);
        xhr.send();
      });

      await WebAssembly.Instantiate(xhr.response, importObject);
      progress(1);
    }
  }

  const wasmFile = "./wasm.wasm";
  await initWasmWithProgress(wasmFile, importObject, p => console.log(`progress: ${p*100}%`));
  console.log("Initialized wasm");