WebVR Bagian 2: Pekerja Web dan Komputasi Tepi Peramban

Diterbitkan: 2022-03-11

Simulator astrofisika kami didukung oleh campuran bahan bakar roket yang kuat dari harapan, sensasi, dan akses ke daya komputasi baru.

Kita dapat mengakses kekuatan komputasi ini dengan pekerja web . Jika Anda sudah terbiasa dengan pekerja web, Anda mungkin ingin memahami kodenya dan langsung beralih ke WebAssembly, yang akan dibahas di artikel berikutnya.

JavaScript menjadi bahasa pemrograman yang paling banyak diinstal, dipelajari, dan dapat diakses karena membawa beberapa fitur yang sangat berguna ke web statis:

  • Loop acara utas tunggal
  • Kode asinkron
  • Pengumpulan sampah
  • Data tanpa pengetikan kaku

Single-threaded berarti kita tidak perlu terlalu khawatir tentang kompleksitas dan perangkap pemrograman multithreaded.

Asynchronous berarti kita dapat menyebarkan fungsi sebagai parameter yang akan dieksekusi nanti - sebagai peristiwa dalam loop peristiwa.

Fitur-fitur ini dan investasi besar-besaran Google dalam kinerja mesin JavaScript V8 Chrome, bersama dengan alat pengembang yang baik menjadikan JavaScript dan Node.js pilihan yang sempurna untuk arsitektur layanan mikro.

Eksekusi single-threaded juga bagus untuk pembuat browser yang harus mengisolasi dan menjalankan semua runtime tab browser yang dipenuhi spyware dengan aman di beberapa inti komputer.

Pertanyaan: Bagaimana satu tab browser dapat mengakses semua inti CPU komputer Anda?
Jawaban: Pekerja web!

Pekerja Web dan Threading

Pekerja web menggunakan loop peristiwa untuk menyampaikan pesan secara asinkron di antara utas, melewati banyak potensi jebakan pemrograman multithread.

Pekerja web juga dapat digunakan untuk memindahkan komputasi dari utas UI utama. Ini memungkinkan utas UI utama menangani klik, animasi, dan mengelola DOM.

Mari kita lihat beberapa kode dari repo GitHub proyek.

Jika Anda ingat diagram arsitektur kami, kami mendelegasikan seluruh simulasi ke nBodySimulator sehingga ia mengelola pekerja web.

diagram arsitektur

Jika Anda ingat dari posting intro, nBodySimulator memiliki fungsi step() yang dipanggil setiap 33ms dari simulasi. Ini memanggil calculateForces() , lalu memperbarui posisi dan mengecat ulang.

 // 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() }

Kontribusi pekerja web adalah menjadi tuan rumah utas terpisah untuk WebAssembly. Sebagai bahasa tingkat rendah, WebAssembly hanya memahami bilangan bulat dan float. Kami tidak dapat meneruskan String atau Objek JavaScript - hanya penunjuk ke "memori linier." Jadi untuk kenyamanan, kami mengemas "tubuh" kami ke dalam array pelampung: arrBodies .

Kami akan kembali ke ini di artikel kami di WebAssembly dan AssemblyScript.

Memindahkan data masuk/keluar dari web worker
Memindahkan data masuk/keluar dari web worker

Di sini, kami membuat pekerja web untuk menjalankan calculateForces() di utas terpisah. Ini terjadi di bawah saat kita menyusun tubuh (x, y, z, massa) ke dalam array float arrBodies , dan kemudian this.worker.postMessage() ke pekerja. Kami mengembalikan janji yang akan diselesaikan pekerja nanti di 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 }

Dari atas, browser GET's index.html yang menjalankan main.js yang membuat new nBodySimulator() dan dalam konstruktornya kita menemukan setupWebWorker() .

n-body-wasm-kanvas

 // nBodySimulator.js /** * Our n-body system simulator */ export class nBodySimulator { constructor() { this.setupWebWorker() ...

new nBodySimulator() kami tinggal di thread UI utama, dan setupWebWorker() membuat web worker dengan mengambil workerWasm.js dari jaringan.

 // 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}`); } ...

Di new Worker() , browser mengambil dan menjalankan workerWasm.js dalam runtime (dan utas) JavaScript terpisah, dan mulai meneruskan pesan.

Kemudian, workerWasm.js masuk ke dalam WebAssembly tetapi sebenarnya hanya satu fungsi this.onmessage() yang berisi pernyataan switch() .

Ingat bahwa pekerja web tidak dapat mengakses jaringan, jadi utas UI utama harus meneruskan kode WebAssembly yang dikompilasi ke pekerja web sebagai pesan resolve("action packed") . Kami akan membahasnya lebih dalam di posting berikutnya.

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

Melompat kembali ke metode setupWebWorker() dari kelas nBodySimulation kita, kita mendengarkan pesan pekerja web menggunakan pola onmessage() + switch() yang sama.

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

Dalam contoh ini, countForces( calculateForces() membuat dan mengembalikan janji yang menyimpan resolve() dan reject() sebagai self.forcesReject() dan self.forcesResolve() .

Dengan cara ini, worker.onmessage() dapat menyelesaikan janji yang dibuat di countForces( calculateForces() .

Jika Anda ingat fungsi step() loop simulasi kami:

 /** * 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}`) }

Ini memungkinkan kita melewati calculForces calculateForces() dan menerapkan kembali kekuatan sebelumnya jika WebAssembly masih menghitung.

Fungsi langkah ini diaktifkan setiap 33 ms. Jika pekerja web belum siap, itu berlaku dan melukis kekuatan sebelumnya. Jika calculateForces() langkah tertentu bekerja melewati awal langkah berikutnya, langkah berikutnya akan menerapkan kekuatan dari posisi langkah sebelumnya. Kekuatan-kekuatan sebelumnya cukup mirip untuk terlihat "benar" atau terjadi begitu cepat sehingga tidak dapat dipahami oleh pengguna. Pertukaran ini meningkatkan kinerja yang dirasakan - bahkan jika itu tidak direkomendasikan untuk perjalanan ruang angkasa manusia yang sebenarnya.

Bisakah ini ditingkatkan? Ya! Alternatif untuk setInterval untuk fungsi langkah kami adalah requestAnimationFrame() .

Untuk tujuan saya, ini cukup baik untuk menjelajahi Canvas, WebVR, dan WebAssembly. Jika Anda yakin ada sesuatu yang bisa ditambahkan atau diganti, jangan ragu untuk berkomentar atau menghubungi kami.

Jika Anda mencari desain mesin fisika yang modern dan lengkap, lihat Matter.js sumber terbuka.

Bagaimana Dengan WebAssembly?

WebAssembly adalah biner portabel yang bekerja di seluruh browser dan sistem. WebAssembly dapat dikompilasi dari banyak bahasa (C/C++/Rust, dll.). Untuk tujuan saya sendiri, saya ingin mencoba AssemblyScript - bahasa yang didasarkan pada TypeScript, yang merupakan bahasa berdasarkan JavaScript, karena semuanya kura-kura.

AssemblyScript mengkompilasi kode TypeScript ke biner "kode objek" portabel, menjadi "just-in-time" yang dikompilasi menjadi runtime berkinerja tinggi baru yang disebut Wasm. Saat mengkompilasi TypeScript ke dalam biner .wasm , dimungkinkan untuk membuat format "teks rakitan web" .wat yang dapat dibaca manusia yang menjelaskan biner.

Bagian terakhir dari setupWebWorker() memulai posting berikutnya di WebAssembly dan menunjukkan cara mengatasi keterbatasan pekerja web dalam mengakses jaringan. Kami fetch() file wasm di utas UI utama, lalu "tepat waktu" mengompilasinya ke modul wasm asli. Kami postMessage() modul itu sebagai pesan ke pekerja web:

 // 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 kemudian membuat instance modul itu sehingga kita dapat memanggil fungsinya:

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

Inilah cara kami mengakses fungsionalitas WebAssembly. Jika Anda melihat kode sumber yang tidak diedit, Anda akan melihat ... adalah sekumpulan kode manajemen memori untuk memasukkan data kami ke dataRef dan hasil kami dari resultRef . Manajemen memori dalam JavaScript? Seru!

Kami akan menggali WebAssembly dan AssemblyScript secara lebih rinci di posting berikutnya.

Batas Eksekusi dan Memori Bersama

Ada hal lain untuk dibicarakan di sini, yaitu batasan eksekusi dan memori bersama.

Empat salinan data Tubuh kita
Empat salinan data Tubuh kita

Artikel WebAssembly sangat taktis, jadi inilah tempat yang bagus untuk membicarakan runtime. JavaScript dan WebAssembly adalah runtime yang "diemulasi". Seperti yang diterapkan, setiap kali kita melewati batas waktu proses, kita membuat salinan data tubuh kita (x, y, z, massa). Meskipun menyalin memori itu murah, ini bukan desain kinerja tinggi yang matang.

Untungnya, banyak orang yang sangat pintar sedang bekerja untuk membuat spesifikasi dan implementasi dari teknologi browser mutakhir ini.

JavaScript memiliki SharedArrayBuffer untuk membuat objek memori bersama yang akan menghilangkan postMessage() dari (2) -> (3) pada panggilan dan onmessage() dari arrForces dari (3) -> (2) pada hasilnya .

WebAssembly juga memiliki desain Memori Linier yang dapat menampung memori bersama untuk panggilan nBodyForces() dari (3) -> (4). Pekerja web juga dapat melewatkan memori bersama untuk larik hasil.

Bergabunglah dengan kami di lain waktu untuk perjalanan yang mengasyikkan ke dalam manajemen memori JavaScript.