WebVRパート2:Webワーカーとブラウザーエッジコンピューティング

公開: 2022-03-11

私たちの天体物理学シミュレーターは、希望、誇大宣伝、そして新しい計算能力へのアクセスの強力なロケット燃料混合物を動力源としています。

このコンピューティングパワーには、 Webワーカーを使用してアクセスできます。 すでにWebワーカーに精通している場合は、コードを理解してWebAssemblyにジャンプすることをお勧めします。これについては、次の記事で説明します。

JavaScriptは、静的Webに非常に便利な機能をもたらしたため、最もインストールされ、学習され、アクセスしやすいプログラミング言語になりました。

  • シングルスレッドイベントループ
  • 非同期コード
  • ガベージコレクション
  • 厳密な入力なしのデータ

シングルスレッドとは、マルチスレッドプログラミングの複雑さと落とし穴についてあまり心配する必要がないことを意味します。

非同期とは、後で実行されるパラメーターとして、つまりイベントループ内のイベントとして関数を渡すことができることを意味します。

これらの機能と、ChromeのV8 JavaScriptエンジンのパフォーマンスへのGoogleの巨額の投資、および優れた開発者ツールにより、JavaScriptとNode.jsはマイクロサービスアーキテクチャに最適な選択肢となりました。

シングルスレッド実行は、コンピューターの複数のコア間でスパイウェアが蔓延しているすべてのブラウザータブランタイムを安全に分離して実行する必要があるブラウザーメーカーにも最適です。

質問: 1つのブラウザタブですべてのコンピュータのCPUコアにアクセスするにはどうすればよいですか?
回答: Webワーカー!

Webワーカーとスレッド

Webワーカーは、イベントループを使用して、スレッド間でメッセージを非同期的に渡し、マルチスレッドプログラミングの潜在的な落とし穴の多くを回避します。

Webワーカーを使用して、メインUIスレッドから計算を移動することもできます。 これにより、メインUIスレッドがクリック、アニメーション、およびDOMの管理を処理できるようになります。

プロジェクトのGitHubリポジトリからいくつかのコードを見てみましょう。

アーキテクチャ図を覚えているなら、シミュレーション全体をnBodySimulatorに委任して、Webワーカーを管理できるようにしました。

アーキテクチャ図

イントロの投稿を思い出してくださいnBodySimulatorには、シミュレーションの33ミリ秒ごとに呼び出されるstep()関数があります。 これは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ワーカーの貢献は、WebAssembly用に別のスレッドをホストすることです。 低水準言語として、WebAssemblyは整数と浮動小数点数のみを理解します。 JavaScriptの文字列オブジェクトを渡すことはできません。「線形メモリ」へのポインタだけです。 したがって、便宜上、「ボディ」をフロートの配列にパッケージ化します: arrBodies

これについては、WebAssemblyとAssemblyScriptに関する記事で説明します。

Webワーカーへのデータの移動/Webワーカーからのデータの移動
Webワーカーへのデータの移動/Webワーカーからのデータの移動

ここでは、別のスレッドでcalculateForces()を実行するWebワーカーを作成しています。 これは、ボディ(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 }

上から、 new nBodySimulator()を作成するmain.jsを実行するブラウザーGETのindex.htmlと、そのコンストラクターにsetupWebWorker()があります。

n-body-wasm-canvas

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

new nBodySimulator()はメインUIスレッドにあり、 setupWebWorker()はネットワークからworkerWasm.jsをフェッチしてWebワーカーを作成します。

 // 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ワーカーはネットワークにアクセスできないため、メインUIスレッドはコンパイルされたWebAssemblyコードをメッセージresolve("action packed")としてWebワーカーに渡す必要があることに注意してください。 これについては、次の投稿で詳しく説明します。

 // 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ワーカーのメッセージをリッスンします。

 // 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()は、repose()とreject()self.forcesReject()およびself.forcesResolve() )として保存resolve() promiseを作成して返します。

このようにして、 worker.onmessage()calculateForces()で作成されたpromiseを解決できます。

シミュレーションループの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}`) }

これにより、 calculateForces()をスキップして、WebAssemblyがまだ計算中の場合は、以前のフォースを再適用できます。

このステップ関数は33msごとに起動します。 Webワーカーの準備ができていない場合は、以前の力を適用してペイントします。 特定のステップのcalculateForces()が次のステップの開始を超えて機能する場合、次のステップは前のステップの位置から力を適用します。 これらの以前の力は、「正しく」見えるように十分に類似しているか、ユーザーが理解できないほど速く発生しています。 このトレードオフにより、実際の人間の宇宙旅行には推奨されない場合でも、知覚されるパフォーマンスが向上します。

これは改善できますか? はい! ステップ関数のsetIntervalの代わりに、 requestAnimationFrame()を使用します。

私の目的では、これはCanvas、WebVR、およびWebAssemblyを探索するのに十分です。 何かが追加または交換される可能性があると思われる場合は、コメントするか、連絡してください。

最新の完全な物理エンジンの設計をお探しの場合は、オープンソースのMatter.jsをご覧ください。

WebAssemblyはどうですか?

WebAssemblyは、ブラウザやシステム間で機能するポータブルバイナリです。 WebAssemblyは、多くの言語(C / C ++ / Rustなど)からコンパイルできます。 私自身の目的のために、私はAssemblyScriptを試してみたかったのです。これは、JavaScriptに基づく言語である、TypeScriptに基づく言語です。これは、ずっとカメであるためです。

AssemblyScriptは、TypeScriptコードをポータブルな「オブジェクトコード」バイナリにコンパイルし、Wasmと呼ばれる新しい高性能ランタイムに「ジャストインタイム」でコンパイルします。 TypeScriptを.wasmバイナリにコンパイルすると、バイナリを説明する.watの人間が読める「Webアセンブリテキスト」形式を作成できます。

setupWebWorker()の最後の部分は、WebAssemblyに関する次の投稿を開始し、ネットワークへのアクセスに関するWebワーカーの制限を克服する方法を示しています。 メインUIスレッドでwasmファイルをfetch()してから、「ジャストインタイム」でネイティブのwasmモジュールにコンパイルします。 そのモジュールをWebワーカーへのメッセージとして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について詳しく説明します。

実行境界と共有メモリ

ここで話すべきことが他にあります。それは実行境界と共有メモリです。

ボディデータの4つのコピー
ボディデータの4つのコピー

WebAssemblyの記事は非常に戦術的であるため、ランタイムについて説明するのに適した場所です。 JavaScriptとWebAssemblyは「エミュレートされた」ランタイムです。 実装されているように、ランタイムの境界を越えるたびに、ボディデータ(x、y、z、mass)のコピーを作成しています。 メモリのコピーは安価ですが、これは成熟した高性能設計ではありません。

幸いなことに、多くの非常に賢い人々が、これらの最先端のブラウザー技術の仕様と実装の作成に取り組んでいます。

JavaScriptにはSharedArrayBufferがあり、呼び出し時にpostMessage postMessage() )の(2)->(3)からのコピーを削除し、結果の(3)->(2)からのarrForcesonmessage()のコピーを削除します。 。

WebAssemblyには、(3)->(4)からのnBodyForces()呼び出し用の共有メモリをホストできる線形メモリ設計もあります。 Webワーカーは、結果配列の共有メモリを渡すこともできます。

次回は、JavaScriptメモリ管理へのエキサイティングな旅にご参加ください。