WebVR ตอนที่ 2: Web Workers และ Browser Edge Computing
เผยแพร่แล้ว: 2022-03-11เครื่องจำลองดาราศาสตร์ฟิสิกส์ของเราขับเคลื่อนโดยส่วนผสมเชื้อเพลิงจรวดอันทรงพลังของความหวัง โฆษณาชวนเชื่อ และการเข้าถึงพลังการประมวลผลแบบใหม่
เราสามารถเข้าถึงพลังการประมวลผลนี้กับ ผู้ทำงานเว็บ หากคุณคุ้นเคยกับ Web Worker อยู่แล้ว คุณอาจต้องการเจาะโค้ดและข้ามไปที่ WebAssembly ซึ่งจะกล่าวถึงในบทความถัดไป
JavaScript กลายเป็นภาษาโปรแกรมที่ติดตั้ง เรียนรู้ และเข้าถึงได้มากที่สุด เพราะมันนำคุณสมบัติที่มีประโยชน์อย่างเหลือเชื่อมาสู่เว็บแบบคงที่:
- ลูปเหตุการณ์แบบเธรดเดียว
- รหัสอะซิงโครนัส
- เก็บขยะ
- ข้อมูลโดยไม่ต้องพิมพ์แบบเข้มงวด
เธรดเดียว หมายความว่าเราไม่ต้องกังวลกับความซับซ้อนและข้อผิดพลาดของการเขียนโปรแกรมแบบมัลติเธรดมากนัก
อะซิงโครนัส หมายความว่าเราสามารถส่งผ่านฟังก์ชันต่างๆ เป็นพารามิเตอร์ที่จะดำเนินการในภายหลัง - เป็นเหตุการณ์ในลูปเหตุการณ์
คุณลักษณะเหล่านี้และการลงทุนมหาศาลของ Google ในด้านประสิทธิภาพของกลไก V8 JavaScript ของ Chrome พร้อมด้วยเครื่องมือสำหรับนักพัฒนาที่ดี ทำให้ JavaScript และ Node.js เป็นตัวเลือกที่สมบูรณ์แบบสำหรับสถาปัตยกรรมไมโครเซอร์วิส
การดำเนินการแบบเธรดเดียวยังยอดเยี่ยมสำหรับผู้ผลิตเบราว์เซอร์ที่ต้องแยกและเรียกใช้รันไทม์แท็บเบราว์เซอร์ที่รบกวนสปายแวร์ทั้งหมดของคุณอย่างปลอดภัยผ่านหลายคอร์ของคอมพิวเตอร์
คำถาม: แท็บเบราว์เซอร์หนึ่งแท็บจะเข้าถึงคอร์ CPU ทั้งหมดของคอมพิวเตอร์ได้อย่างไร
คำตอบ: พนักงานเว็บ!
Web Workers และ Threading
พนักงานเว็บใช้การวนซ้ำของเหตุการณ์เพื่อส่งข้อความระหว่างเธรดแบบอะซิงโครนัส โดยข้ามข้อผิดพลาดที่อาจเกิดขึ้นมากมายของการเขียนโปรแกรมแบบมัลติเธรด
ผู้ใช้เว็บสามารถใช้เพื่อย้ายการคำนวณออกจากเธรด UI หลักได้ ซึ่งช่วยให้เธรด UI หลักจัดการกับการคลิก ภาพเคลื่อนไหว และการจัดการ DOM
มาดูโค้ดบางส่วนจาก GitHub repo ของโปรเจ็กต์กัน
หากคุณจำไดอะแกรมสถาปัตยกรรมของเราได้ เราได้มอบหมายการจำลองทั้งหมดให้กับ nBodySimulator
เพื่อให้จัดการผู้ปฏิบัติงานบนเว็บ
หากคุณจำได้จากโพสต์แนะนำ 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() }
การสนับสนุนของผู้ปฏิบัติงานบนเว็บคือการโฮสต์เธรดแยกต่างหากสำหรับ WebAssembly ในฐานะภาษาระดับต่ำ WebAssembly จะเข้าใจเฉพาะจำนวนเต็มและทศนิยมเท่านั้น เราไม่สามารถส่งผ่าน JavaScript Strings หรือ Objects ได้ เพียงแค่ชี้ไปที่ “หน่วยความจำเชิงเส้น” ดังนั้น เพื่อความสะดวก เราจึงจัดแพ็คเกจ "ร่าง" ของเราเป็นชุดลอย: arrBodies
เราจะกลับมาที่นี่ในบทความของเราเกี่ยวกับ WebAssembly และ AssemblyScript
ที่นี่ เรากำลังสร้าง Web Worker เพื่อเรียกใช้ calculateForces()
ในเธรดแยกต่างหาก สิ่งนี้เกิดขึ้นด้านล่างเมื่อเรารวมร่าง (x, y, z, มวล) ลงในอาร์เรย์ของ floats 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 }
จากด้านบนสุดคือ index.html
ของเบราว์เซอร์ GET ซึ่งรัน main.js
ซึ่งสร้าง new nBodySimulator()
และใน Constructor เราพบ setupWebWorker()
// nBodySimulator.js /** * Our n-body system simulator */ export class nBodySimulator { constructor() { this.setupWebWorker() ...
new nBodySimulator()
ของเราอาศัยอยู่ในเธรด UI หลัก และ setupWebWorker()
จะสร้างผู้ปฏิบัติงานบนเว็บโดยการดึง workerWasm.js
จากเครือข่าย
// 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()
เบราว์เซอร์จะดึงและเรียก workerWasm.js
ในรันไทม์ JavaScript ที่แยกต่างหาก (และเธรด) และเริ่มส่งข้อความ
จากนั้น workerWasm.js
จะเข้าสู่จุดแข็งของ WebAssembly แต่จริงๆ แล้วเป็นเพียง this.onmessage()
ฟังก์ชันเดียวที่มีคำสั่ง switch()
โปรดจำไว้ว่าผู้ปฏิบัติงานเว็บไม่สามารถเข้าถึงเครือข่ายได้ ดังนั้นเธรด UI หลักต้องส่งโค้ด WebAssembly ที่คอมไพล์ไปยังผู้ทำงานเว็บเพื่อ 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 }) } }
ย้อนกลับไปที่ setupWebWorker()
ของคลาส nBodySimulation
เราฟังข้อความของผู้ทำงานเว็บโดยใช้รูปแบบ onmessage() + switch()
เดียวกัน

// 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()
สามารถแก้ไขสัญญาที่สร้างขึ้นใน calcForces( 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}`) }
ซึ่งช่วยให้เราข้าม calcForces calculateForces()
และใช้แรงก่อนหน้านี้อีกครั้งหาก WebAssembly ยังคงคำนวณอยู่
ฟังก์ชันขั้นตอนนี้จะเริ่มทำงานทุกๆ 33ms หากผู้ปฏิบัติงานเว็บไม่พร้อม จะใช้และระบายสีกองกำลังก่อนหน้า หาก calculateForces()
ของขั้นตอนหนึ่งทำงานหลังจากเริ่มขั้นตอนถัดไป ขั้นตอนต่อไปจะใช้แรงจากตำแหน่งของขั้นตอนก่อนหน้า แรงก่อนหน้านั้นคล้ายกันมากจนดู "ถูกต้อง" หรือเกิดขึ้นเร็วจนผู้ใช้เข้าใจยาก การแลกเปลี่ยนนี้ช่วยเพิ่มประสิทธิภาพการรับรู้ แม้ว่าจะไม่แนะนำสำหรับการเดินทางในอวกาศของมนุษย์จริงๆ
สิ่งนี้สามารถปรับปรุงได้หรือไม่? ใช่! ทางเลือกอื่นสำหรับ setInterval
สำหรับฟังก์ชันขั้นตอนของเราคือ requestAnimationFrame()
สำหรับจุดประสงค์ของฉัน สิ่งนี้ดีพอที่จะสำรวจ Canvas, WebVR และ WebAssembly หากคุณเชื่อว่ามีบางสิ่งที่สามารถเพิ่มหรือแลกเปลี่ยนได้ โปรดแสดงความคิดเห็นหรือติดต่อกลับ
หากคุณกำลังมองหาการออกแบบเอ็นจิ้นฟิสิกส์ที่ทันสมัยและสมบูรณ์แบบ ลองดู Matter.js โอเพ่นซอร์ส
สิ่งที่เกี่ยวกับ WebAssembly?
WebAssembly เป็นไบนารีแบบพกพาที่ทำงานข้ามเบราว์เซอร์และระบบ WebAssembly สามารถคอมไพล์ได้จากหลายภาษา (C/C++/Rust ฯลฯ) เพื่อจุดประสงค์ของฉันเอง ฉันต้องการลองใช้ AssemblyScript - ภาษาที่ใช้ TypeScript ซึ่งเป็นภาษาที่ใช้ JavaScript เพราะเป็นเต่าลงไป
AssemblyScript รวบรวมโค้ด TypeScript ให้เป็นไบนารี "โค้ดอ็อบเจ็กต์" แบบพกพา เพื่อคอมไพล์แบบ "ทันเวลาพอดี" เป็นรันไทม์ประสิทธิภาพสูงตัวใหม่ที่เรียกว่า Wasm เมื่อรวบรวม TypeScript ลงในไบนารี .wasm
คุณสามารถสร้างรูปแบบ "ข้อความประกอบเว็บ" ที่มนุษย์อ่านได้ .wat
ซึ่งอธิบายไบนารี
ส่วนสุดท้ายของ setupWebWorker()
เริ่มต้นโพสต์ถัดไปของเราใน WebAssembly และแสดงวิธีเอาชนะข้อจำกัดของผู้ปฏิบัติงานบนเว็บในการเข้าถึงเครือข่าย เรา fetch()
ไฟล์ wasm
ในเธรด UI หลัก จากนั้น "just-in-time" คอมไพล์ลงในโมดูล 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, มวล) แม้ว่าการคัดลอกหน่วยความจำจะมีราคาถูก แต่นี่ไม่ใช่การออกแบบที่มีประสิทธิภาพสูงสำหรับผู้ใหญ่
โชคดีที่คนฉลาดๆ จำนวนมากกำลังทำงานเพื่อสร้างข้อกำหนดและการใช้งานเทคโนโลยีเบราว์เซอร์ล้ำสมัยเหล่านี้
JavaScript มี SharedArrayBuffer เพื่อสร้างวัตถุหน่วยความจำที่ใช้ร่วมกันซึ่งจะกำจัด postMessage()
จาก (2) -> (3) ในการโทรและสำเนา onmessage()
จาก (3) -> (2) ของ arrForces
() จาก (3) -> (2) .
WebAssembly ยังมีการออกแบบหน่วยความจำเชิงเส้นที่สามารถโฮสต์หน่วยความจำที่ใช้ร่วมกันสำหรับการ nBodyForces()
จาก (3) -> (4) พนักงานเว็บยังสามารถส่งหน่วยความจำที่ใช้ร่วมกันสำหรับอาร์เรย์ผลลัพธ์ได้อีกด้วย
เข้าร่วมกับเราในครั้งต่อไปสำหรับการเดินทางที่น่าตื่นเต้นในการจัดการหน่วยความจำ JavaScript