WebVR 第 2 部分:Web Workers 和浏览器边缘计算
已发表: 2022-03-11我们的天体物理学模拟器由一种强大的火箭燃料混合物提供动力,该燃料混合了希望、炒作和获得新计算能力的途径。
我们可以通过web workers访问这种计算能力。 如果您已经熟悉 Web Workers,您可能想深入了解代码并跳到 WebAssembly,这将在下一篇文章中讨论。
JavaScript 成为安装、学习和访问最多的编程语言,因为它为静态 Web 带来了一些非常有用的功能:
- 单线程事件循环
- 异步代码
- 垃圾收集
- 没有严格类型的数据
单线程意味着我们不必过多担心多线程编程的复杂性和陷阱。
异步意味着我们可以传递函数作为稍后执行的参数——作为事件循环中的事件。
这些特性和谷歌对 Chrome 的 V8 JavaScript 引擎性能的大量投资,以及优秀的开发工具,使 JavaScript 和 Node.js 成为微服务架构的完美选择。
单线程执行也非常适合浏览器制造商,他们必须跨计算机的多个内核安全地隔离和运行所有受间谍软件感染的浏览器选项卡运行时。
问题:一个浏览器选项卡如何访问您计算机的所有 CPU 内核?
答:网络工作者!
Web Worker 和线程
Web Worker 使用事件循环在线程之间异步传递消息,绕过了多线程编程的许多潜在缺陷。
Web worker 也可用于将计算移出主 UI 线程。 这让主 UI 线程可以处理点击、动画和管理 DOM。
让我们看一下该项目的 GitHub 存储库中的一些代码。
如果您还记得我们的架构图,我们将整个模拟委托给nBodySimulator
,以便它管理 Web Worker。
如果您还记得介绍文章中的内容, nBodySimulator
有一个step()
函数,每 33ms 的模拟调用一次。 它调用calculateForces()
,然后更新位置并重新绘制。
// Methods from class nBodySimulator /** * The simulation loop */ start() { // This is the simulation loop. step() calls visualize() const step = this.step.bind(this) setInterval(step, this.simulationSpeed) } /** * A step in the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps), expect it to skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: WorkerReady: ${this.workerReady} WorkerCalculating: ${this.workerCalculating}`) } // Remove any "debris" that has traveled out of bounds - this is for the button this.trimDebris() // Now Update forces. Reuse old forces if we skipped calculateForces() above this.applyForces() // Ta-dah! this.visualize() }
Web Worker 的贡献是为 WebAssembly 托管一个单独的线程。 作为一种低级语言,WebAssembly 只理解整数和浮点数。 我们不能传递 JavaScript字符串或对象——只是指向“线性内存”的指针。 所以为了方便起见,我们将我们的“body”打包成一个浮点数组: arrBodies
。
我们将在关于 WebAssembly 和 AssemblyScript 的文章中再次讨论这一点。
在这里,我们创建了一个 web worker 来在一个单独的线程中运行calculateForces()
。 这发生在下面,因为我们将物体 (x, y, z, mass) 编组为浮点数arrBodies
,然后将this.worker.postMessage()
到工人。 我们返回一个承诺,工人将在this.worker.onMessage()
稍后解决。
// src/nBodySimulator.js /** * Use our web worker to calculate the forces to apply on our bodies. */ calculateForces() { this.workerCalculating = true this.arrBodies = [] // Copy data to array into this.arrBodies ... // return promise that worker.onmessage will fulfill const ret = new Promise((resolve, reject) => { this.forcesResolve = resolve this.forcesReject = reject }) // postMessage() to worker to start calculation // Execution continues in workerWasm.js worker.onmessage() this.worker.postMessage({ purpose: 'nBodyForces', arrBodies: this.arrBodies, }) // Return promise for completion // Promise is resolve()d in this.worker.onmessage() below. // Once resolved, execution continues in step() above - await this.calculateForces() return ret }
从顶部开始,浏览器 GET 的index.html
运行main.js
,它创建一个new nBodySimulator()
并在其构造函数中找到setupWebWorker()
。
// nBodySimulator.js /** * Our n-body system simulator */ export class nBodySimulator { constructor() { this.setupWebWorker() ...
我们的new nBodySimulator()
位于主 UI 线程中, setupWebWorker()
通过从网络中获取workerWasm.js
来创建 web worker。
// nBodySimulator.js // Main UI thread - Class nBodySimulator method setupWebWorker() { // Create a web worker (separate thread) that we'll pass the WebAssembly module to. this.worker = new Worker("workerWasm.js"); // Console errors from workerWasm.js this.worker.onerror = function (evt) { console.log(`Error from web worker: ${evt.message}`); } ...
在new Worker()
中,浏览器在单独的 JavaScript 运行时(和线程)中获取并运行workerWasm.js
,并开始传递消息。
然后, workerWasm.js
进入了 WebAssembly,但它实际上只是一个包含switch()
语句的this.onmessage()
函数。
请记住,Web Worker 无法访问网络,因此主 UI 线程必须将编译后的 WebAssembly 代码作为消息传递给 Web Worker resolve("action packed")
。 我们将在下一篇文章中深入探讨。
// workerWasm.js - runs in a new, isolated web worker runtime (and thread) this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. ... // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return // Message: Given array of floats describing a system of bodies (x, y, z, mass), // calculate the Grav forces to be applied to each body case 'nBodyForces': ... // Do the calculations in this web worker thread synchronously const resultRef = wasm.nBodyForces(dataRef); ... // See nBodySimulation.js' this.worker.onmessage return this.postMessage({ purpose: 'nBodyForces', arrForces }) } }
回到我们的nBodySimulation
类的setupWebWorker()
方法,我们使用相同的onmessage() + switch()
模式来监听 web worker 的消息。

// Continuing class nBodySimulator's setupWebWorker() in the main UI thread // Listen for messages from workerWasm.js postMessage() const self = this this.worker.onmessage = function (evt) { if (evt && evt.data) { // Messages are dispatched by purpose const msg = evt.data switch (msg.purpose) { // Worker's reply that it has loaded the wasm module we compiled and sent. Let the magic begin! // See postmessage at the bottom of this function. case 'wasmReady': self.workerReady = true break // wasm has computed forces for us // Response to postMessage() in nBodySimulator.calculateForces() above case 'nBodyForces': self.workerCalculating = false // Resolve await this.calculateForces() in step() above if (msg.error) { self.forcesReject(msg.error) } else { self.arrForces = msg.arrForces self.forcesResolve(self.arrForces) } break } } } ...
在这个例子中, calculateForces()
创建并返回一个保存resolve()
和reject()
作为self.forcesReject()
和self.forcesResolve()
的承诺。
这样, worker.onmessage()
可以解决在calculateForces()
中创建的承诺。
如果你还记得我们模拟循环的step()
函数:
/** * This is the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps), expect it to skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: WorkerReady: ${this.workerReady} WorkerCalculating: ${this.workerCalculating}`) }
如果 WebAssembly 仍在计算,这让我们可以跳过calculateForces()
并重新应用之前的力。
此步进函数每 33 毫秒触发一次。 如果 web worker 没有准备好,它会应用并绘制先前的力量。 如果特定步骤的calculateForces()
在下一步开始之后起作用,则下一步将从上一步的位置施加力。 这些先前的力量要么相似到看起来“正确”,要么发生得如此之快以至于用户无法理解。 这种权衡提高了感知性能——即使不建议将其用于实际的人类太空旅行。
这可以改进吗? 是的! 对于我们的 step 函数, setInterval
的替代方法是requestAnimationFrame()
。
就我而言,这足以探索 Canvas、WebVR 和 WebAssembly。 如果您认为可以添加或更换某些内容,请随时发表评论或与我们联系。
如果您正在寻找现代的、完整的物理引擎设计,请查看开源的 Matter.js。
WebAssembly 怎么样?
WebAssembly 是一个可移植的二进制文件,可以跨浏览器和系统工作。 WebAssembly 可以从多种语言(C/C++/Rust 等)编译。 出于我自己的目的,我想尝试 AssemblyScript——一种基于 TypeScript 的语言,它是一种基于 JavaScript 的语言,因为它一直是乌龟。
AssemblyScript 将 TypeScript 代码编译为可移植的“目标代码”二进制文件,以便“即时”编译到名为 Wasm 的新高性能运行时中。 将 TypeScript 编译成.wasm
二进制文件时,可以创建一个.wat
人类可读的“Web 汇编文本”格式来描述二进制文件。
setupWebWorker()
的最后一部分开始了我们关于 WebAssembly 的下一篇文章,并展示了如何克服 Web Worker 对网络访问的限制。 我们在主 UI 线程中fetch()
wasm
文件,然后“即时”将其编译为原生 wasm 模块。 我们postMessage()
将该模块作为消息发送给网络工作者:
// completing setupWebWorker() in the main UI thread … // Fetch and compile the wasm module because web workers cannot fetch() WebAssembly.compileStreaming(fetch("assembly/nBodyForces.wasm")) // Send the compiled wasm module to the worker as a message .then(wasmModule => { self.worker.postMessage({ purpose: 'wasmModule', wasmModule }) }); } }
workerWasm.js
然后实例化该模块,以便我们可以调用它的函数:
// wasmWorker.js - web worker onmessage function this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. wasm = loader.instantiate(msg.wasmModule, importObj) // Throws // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return case 'nBodyForces': ... // Do the calculations in this thread synchronously const resultRef = wasm.nBodyForces(dataRef);
这就是我们访问 WebAssembly 功能的方式。 如果您正在查看未编辑的源代码,您会注意到...
是一堆内存管理代码,用于将我们的数据放入dataRef
并将我们的结果从resultRef
中取出。 JavaScript 中的内存管理? 令人兴奋!
我们将在下一篇文章中更详细地研究 WebAssembly 和 AssemblyScript。
执行边界和共享内存
这里还有一些东西要谈,那就是执行边界和共享内存。
WebAssembly 的文章很有战术性,所以这里是讨论运行时的好地方。 JavaScript 和 WebAssembly 是“模拟的”运行时。 正如实现的那样,每次我们跨越运行时边界时,我们都会复制我们的身体数据(x,y,z,mass)。 虽然复制内存很便宜,但这并不是成熟的高性能设计。
幸运的是,许多非常聪明的人正在致力于创建这些尖端浏览器技术的规范和实现。
JavaScript 具有 SharedArrayBuffer 来创建一个共享内存对象,该对象将消除调用时从 (2) -> (3) 的postMessage()
的副本以及从 (3) -> (2) 的arrForces
的onmessage()
的副本。 .
WebAssembly 还具有线性内存设计,可以为 (3) -> (4) 的nBodyForces()
调用托管共享内存。 web worker 也可以为结果数组传递一个共享内存。
下次加入我们,开启一段激动人心的 JavaScript 内存管理之旅。