WebVR Bölüm 2: Web Çalışanları ve Tarayıcı Kenarında Hesaplama

Yayınlanan: 2022-03-11

Astrofizik simülatörümüz, umut, heyecan ve yeni bilgi işlem gücüne erişimin güçlü bir roket yakıtı karışımıyla desteklenmektedir.

Bu bilgi işlem gücüne web çalışanları ile erişebiliriz. Web çalışanları hakkında zaten bilginiz varsa, kodu gözden geçirip bir sonraki makalede ele alınacak olan WebAssembly'ye geçmek isteyebilirsiniz.

JavaScript, statik web'e inanılmaz derecede faydalı özellikler getirdiği için en çok yüklenen, öğrenilen ve erişilebilir programlama dili oldu:

  • Tek iş parçacıklı olay döngüsü
  • asenkron kod
  • Çöp toplama
  • Katı yazma olmadan veriler

Tek iş parçacıklı , çok iş parçacıklı programlamanın karmaşıklığı ve tuzakları hakkında fazla endişelenmemize gerek olmadığı anlamına gelir.

Eşzamansız , işlevleri daha sonra yürütülecek parametreler olarak - olay döngüsündeki olaylar olarak geçirebileceğimiz anlamına gelir.

Bu özellikler ve Google'ın Chrome'un V8 JavaScript motorunun performansına yaptığı büyük yatırım ve iyi geliştirici araçları JavaScript ve Node.js'yi mikro hizmet mimarileri için mükemmel bir seçim haline getirdi.

Tek iş parçacıklı yürütme, casus yazılım bulaşmış tüm tarayıcı sekmesi çalışma zamanlarınızı bir bilgisayarın birden çok çekirdeğinde güvenli bir şekilde yalıtmak ve çalıştırmak zorunda olan tarayıcı üreticileri için de harikadır.

Soru: Bir tarayıcı sekmesi bilgisayarınızın tüm CPU çekirdeklerine nasıl erişebilir?
Cevap: Web çalışanları!

Web Çalışanları ve İş Parçacığı

Web çalışanları, çok iş parçacıklı programlamanın olası tuzaklarının çoğunu atlayarak, ileti dizileri arasında eşzamansız olarak iletmek için olay döngüsünü kullanır.

Web çalışanları, hesaplamayı ana UI iş parçacığının dışına taşımak için de kullanılabilir. Bu, ana UI iş parçacığının tıklamaları, animasyonu ve DOM'yi yönetmesini sağlar.

Projenin GitHub deposundan bazı kodlara bakalım.

Mimari diyagramımızı hatırlarsanız, tüm simülasyonu web çalışanını yönetmesi için nBodySimulator .

Mimari diyagramı

Giriş yazısından hatırlarsanız, nBodySimulator simülasyonun her 33ms'sinde bir step() işlevine sahiptir. calculateForces() öğesini çağırır, ardından konumları günceller ve yeniden boyar.

 // 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 çalışanının katkısı, WebAssembly için ayrı bir iş parçacığı barındırmaktır. Düşük seviyeli bir dil olan WebAssembly, yalnızca tamsayıları ve kayan noktaları anlar. JavaScript Dizelerini veya Nesnelerini iletemiyoruz - yalnızca "doğrusal belleğe" işaretçiler. Bu nedenle, kolaylık olması için, "gövdelerimizi" bir dizi yüzer olarak paketliyoruz: arrBodies .

WebAssembly ve AssemblyScript hakkındaki makalemizde buna geri döneceğiz.

Web çalışanının içine/dışarı veri taşıma
Web çalışanının içine/dışarı veri taşıma

Burada, ayrı bir iş parçacığında calculateForces() 'i çalıştırmak için bir web işçisi oluşturuyoruz. Bu, gövdeleri (x, y, z, mass) bir dizi arrBodies ve ardından this.worker.postMessage() işçiye sıraladığımızda gerçekleşir. Çalışanın daha sonra this.worker.onMessage() içinde çözeceği bir söz veriyoruz.

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

En üstte, new nBodySimulator() oluşturan main.js çalıştıran tarayıcı GET'in index.html ve oluşturucusunda setupWebWorker( setupWebWorker() buluyoruz.

n-vücut-wasm-tuval

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

new nBodySimulator() , ana UI iş parçacığında bulunur ve setupWebWorker() , workerWasm.js ağdan getirerek web çalışanını oluşturur.

 // 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() 'da tarayıcı, workerWasm.js ayrı bir JavaScript çalışma zamanında (ve iş parçacığında) alır ve çalıştırır ve iletileri iletmeye başlar.

Ardından, workerWasm.js girer, ancak bu gerçekten bir switch() deyimi içeren tek bir this.onmessage() işlevidir.

Web çalışanlarının ağa erişemeyeceğini unutmayın, bu nedenle ana UI iş parçacığının derlenmiş WebAssembly kodunu web çalışanına bir mesajsolve resolve("action packed") olarak iletmesi gerekir. Bir sonraki yazıda buna derinlemesine gireceğiz.

 // 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() yöntemine geri dönerek, aynı onmessage() + switch() modelini kullanarak web çalışanının mesajlarını dinliyoruz.

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

Bu örnekte, calculateForces() (),solve( resolve() ve reject() 'i self.forcesReject() ve self.forcesResolve() ) olarak kaydeden bir söz oluşturur ve döndürür.

Bu şekilde, worker.onmessage() , hesapForces( calculateForces() içinde oluşturulan vaadi çözebilir.

Simülasyon döngümüzün step() fonksiyonunu hatırlarsanız:

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

Bu, WebAssembly hala hesaplama yapıyorsa, calculateForces() öğesini atlamamızı ve önceki kuvvetleri yeniden uygulamamızı sağlar.

Bu adım işlevi her 33 ms'de bir tetiklenir. Web işçisi hazır değilse önceki kuvvetleri uygular ve boyar. Belirli bir adımın calculateForces() işlevi sonraki adımın başlangıcından sonra çalışırsa, sonraki adım, önceki adımın konumundan kuvvetleri uygular. Bu önceki kuvvetler ya "doğru" görünecek kadar benzerdir ya da kullanıcı için anlaşılmaz olacak kadar hızlı gerçekleşir. Bu değiş tokuş algılanan performansı artırır - gerçek insan uzay yolculuğu için önerilmese bile.

Bu geliştirilebilir mi? Evet! Adım işlevimiz için setInterval bir alternatif requestAnimationFrame() 'dir.

Amacım için bu, Canvas, WebVR ve WebAssembly'yi keşfetmek için yeterince iyi. Bir şeyin eklenebileceğini veya değiştirilebileceğini düşünüyorsanız, yorum yapmaktan veya iletişime geçmekten çekinmeyin.

Modern, eksiksiz bir fizik motoru tasarımı arıyorsanız, açık kaynaklı Matter.js'ye göz atın.

WebAssembly Hakkında?

WebAssembly, tarayıcılar ve sistemler arasında çalışan taşınabilir bir ikili dosyadır. WebAssembly birçok dilden derlenebilir (C/C++/Rust, vb.). Kendi amacım için, JavaScript'e dayalı bir dil olan TypeScript'e dayalı bir dil olan AssemblyScript'i denemek istedim, çünkü tamamen kaplumbağalar.

AssemblyScript, TypeScript kodunu, Wasm adlı yeni bir yüksek performanslı çalışma zamanında "tam zamanında" derlenecek şekilde taşınabilir bir "nesne kodu" ikili dosyasında derler. TypeScript'i .wasm ikili dosyasına derlerken, ikiliyi açıklayan bir .wat insan tarafından okunabilir “web derleme metni” formatı oluşturmak mümkündür.

setupWebWorker() 'ın son kısmı, WebAssembly'deki bir sonraki yazımıza başlar ve web çalışanının ağa erişim sınırlamalarının nasıl üstesinden gelineceğini gösterir. Ana UI iş parçacığındaki wasm dosyasını alırız fetch() , ardından "tam zamanında" onu yerel bir wasm modülüne derleriz. Bu modülü, web çalışanına bir mesaj olarak Mesaj postMessage() göndeririz:

 // 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 daha sonra bu modülü başlatır, böylece işlevlerini çağırabiliriz:

 // 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 işlevine bu şekilde erişiyoruz. Düzeltilmemiş kaynak koduna bakıyorsanız, verilerimizi dataRef ve sonuçlarımızı resultRef almak için bir grup bellek yönetim kodu ... fark edeceksiniz. JavaScript'te bellek yönetimi? Heyecan verici!

Bir sonraki gönderide WebAssembly ve AssemblyScript'i daha ayrıntılı olarak inceleyeceğiz.

Yürütme Sınırları ve Paylaşılan Bellek

Burada konuşacak başka bir şey daha var, o da yürütme sınırları ve paylaşılan hafıza.

Beden verilerimizin dört kopyası
Beden verilerimizin dört kopyası

WebAssembly makalesi çok taktikseldir, bu nedenle çalışma zamanları hakkında konuşmak için iyi bir yer. JavaScript ve WebAssembly, "öykünülmüş" çalışma zamanlarıdır. Uygulandığı gibi, bir çalışma zamanı sınırını her geçtiğimizde, vücut verilerimizin (x, y, z, kütle) bir kopyasını oluşturuyoruz. Belleği kopyalamak ucuz olsa da, bu olgunlaşmış yüksek performanslı bir tasarım değildir.

Neyse ki, çok sayıda akıllı insan bu son teknoloji tarayıcı teknolojilerinin özelliklerini ve uygulamalarını oluşturmak için çalışıyor.

JavaScript, çağrıda (2) -> (3) onmessage() postMessage() kopyasını ve sonuçta (3) -> (2) arrForces gelen arrForces öğesinin onmessage() kopyasını ortadan kaldıracak bir paylaşılan bellek nesnesi oluşturmak için SharedArrayBuffer'a sahiptir. .

WebAssembly ayrıca (3) -> (4) nBodyForces() çağrısı için paylaşılan bir belleği barındırabilen bir Doğrusal Bellek tasarımına sahiptir. Web çalışanı, sonuç dizisi için paylaşılan bir bellek de iletebilir.

JavaScript bellek yönetimine yönelik heyecan verici bir yolculuk için bir dahaki sefere bize katılın.