WebVR الجزء 2: عمال الويب وحوسبة المتصفح
نشرت: 2022-03-11يتم تشغيل محاكي الفيزياء الفلكية الخاص بنا بخليط قوي من وقود الصواريخ من الأمل والضجيج والوصول إلى قوة الحوسبة الجديدة.
يمكننا الوصول إلى قوة الحوسبة هذه مع العاملين على الويب . إذا كنت معتادًا بالفعل على العاملين على الويب ، فقد ترغب في تحليل التعليمات البرمجية والانتقال إلى WebAssembly ، والذي سيتم مناقشته في المقالة التالية.
أصبحت JavaScript لغة البرمجة الأكثر تثبيتًا وتعلمًا ويمكن الوصول إليها لأنها جلبت بعض الميزات المفيدة بشكل لا يصدق إلى الويب الثابت:
- حلقة حدث مترابطة واحدة
- كود غير متزامن
- جمع القمامة
- البيانات بدون كتابة جامدة
يعني الخيط المفرد أنه لا داعي للقلق كثيرًا بشأن تعقيد ومخاطر البرمجة متعددة مؤشرات الترابط.
يعني غير المتزامن أنه يمكننا تمرير الوظائف كمعلمات يتم تنفيذها لاحقًا - كأحداث في حلقة الحدث.
هذه الميزات واستثمارات Google الهائلة في أداء محرك Chrome's V8 JavaScript ، جنبًا إلى جنب مع أدوات المطورين الجيدة ، جعلت JavaScript و Node.js الخيار الأمثل لبنى الخدمات المصغرة.
يعد التنفيذ أحادي السلسلة أيضًا رائعًا لصانعي المستعرضات الذين يتعين عليهم عزل وتشغيل جميع أوقات تشغيل علامة تبويب المتصفح المليئة ببرامج التجسس عبر مراكز الكمبيوتر المتعددة.
سؤال: كيف يمكن لعلامة تبويب متصفح واحدة الوصول إلى جميع نوى وحدة المعالجة المركزية بجهاز الكمبيوتر الخاص بك؟
الجواب: عمال الويب!
عمال الويب والخيوط
يستخدم العاملون على الويب حلقة الحدث لتمرير الرسائل بشكل غير متزامن بين سلاسل الرسائل ، متجاوزين العديد من المزالق المحتملة للبرمجة متعددة مؤشرات الترابط.
يمكن أيضًا استخدام عمال الويب لنقل الحساب خارج مؤشر ترابط واجهة المستخدم الرئيسي. يتيح هذا لواجهة المستخدم الرئيسية التعامل مع النقرات والرسوم المتحركة وإدارة DOM.
دعونا نلقي نظرة على بعض التعليمات البرمجية من مشروع GitHub repo.
إذا كنت تتذكر الرسم التخطيطي للهندسة المعمارية ، فقد فوضنا المحاكاة بأكملها إلى nBodySimulator
بحيث يدير عامل الويب.
إذا كنت تتذكر من منشور المقدمة ، nBodySimulator
له وظيفة step()
تسمى كل 33 مللي ثانية من المحاكاة. يقوم باستدعاء 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 - فقط مؤشرات إلى "الذاكرة الخطية". لذلك ، من أجل الراحة ، نقوم بتجميع "أجسادنا" في مجموعة من العوامات: arrBodies
.
سنعود إلى هذا في مقالتنا حول WebAssembly و AssemblyScript.
هنا ، نقوم بإنشاء عامل ويب لتشغيل calculateForces()
في سلسلة منفصلة. يحدث هذا أدناه عندما ننظم الأجسام (x ، y ، z ، الكتلة) في مجموعة من الأجسام العائمة ، ثم 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 }
من الأعلى ، المتصفح GET's index.html
الذي يقوم بتشغيل main.js
الذي ينشئ new nBodySimulator()
وفي منشئه نجد setupWebWorker()
.
// nBodySimulator.js /** * Our n-body system simulator */ export class nBodySimulator { constructor() { this.setupWebWorker() ...
يعيش new nBodySimulator()
في مؤشر ترابط واجهة المستخدم الرئيسي ، ويقوم 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()
.
تذكر أن العاملين على الويب لا يمكنهم الوصول إلى الشبكة ، لذلك يجب أن يقوم مؤشر ترابط واجهة المستخدم الرئيسي بتمرير رمز 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()
حل الوعد الذي تم إنشاؤه في 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}`) }
يتيح لنا ذلك تخطي calculateForces()
وإعادة تطبيق القوى السابقة إذا كان WebAssembly لا يزال قيد الحساب.
تعمل وظيفة الخطوة هذه كل 33 مللي ثانية. إذا لم يكن عامل الويب جاهزًا ، فإنه يطبق ويرسم القوى السابقة. إذا نجحت 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
في مؤشر ترابط واجهة المستخدم الرئيسي ، ثم نقوم بتجميعه "في الوقت المناسب" إلى وحدة 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 تكتيكية للغاية ، لذا يوجد هنا مكان جيد للحديث عن أوقات التشغيل. جافا سكريبت و WebAssembly هما أوقات تشغيل "تمت مضاهاتها". كما تم تنفيذه ، في كل مرة نعبر فيها حدود وقت التشغيل ، نقوم بعمل نسخة من بيانات الجسم (x ، y ، z ، الكتلة). في حين أن نسخ الذاكرة رخيص ، إلا أن هذا ليس تصميمًا ناضجًا وعالي الأداء.
لحسن الحظ ، يعمل الكثير من الأشخاص الأذكياء جدًا على إنشاء المواصفات والتطبيقات لتقنيات المستعرضات المتطورة هذه.
يحتوي JavaScript على SharedArrayBuffer لإنشاء كائن ذاكرة مشترك من شأنه إزالة postMessage()
من (2) -> (3) على المكالمة onmessage()
من arrForces
من (3) -> (2) في النتيجة .
يحتوي WebAssembly أيضًا على تصميم ذاكرة خطي يمكنه استضافة ذاكرة مشتركة nBodyForces()
من (3) -> (4). يمكن لعامل الويب أيضًا تمرير ذاكرة مشتركة لمصفوفة النتائج.
انضم إلينا في المرة القادمة في رحلة مثيرة في إدارة ذاكرة JavaScript.