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
การย้ายข้อมูลเข้า/ออกจาก Web Worker

ที่นี่ เรากำลังสร้าง 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()

n-body-wasm-ผ้าใบ

 // 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