यह गाइड, वेब डेवलपर के लिए है. इसमें बताया गया है कि WebAssembly का इस्तेमाल करके, सीपीयू पर ज़्यादा लोड डालने वाले टास्क को आउटसोर्स कैसे किया जा सकता है. इसके लिए, एक उदाहरण भी दिया गया है. इस गाइड में, Wasm मॉड्यूल लोड करने के सबसे सही तरीकों से लेकर, उनके कंपाइलेशन और इंस्टैंटिएशन को ऑप्टिमाइज़ करने तक की सभी जानकारी शामिल है. इसमें सीपीयू का ज़्यादा इस्तेमाल करने वाले टास्क को वेब वर्कर पर ट्रांसफ़र करने के बारे में भी बताया गया है. साथ ही, इसमें लागू करने से जुड़े उन फ़ैसलों के बारे में भी बताया गया है जो आपको लेने होंगे. जैसे, वेब वर्कर कब बनाना है और इसे हमेशा चालू रखना है या ज़रूरत पड़ने पर चालू करना है. इस गाइड में, समस्या को हल करने के तरीके को लगातार बेहतर बनाया जाता है. साथ ही, एक बार में परफ़ॉर्मेंस से जुड़ा एक पैटर्न पेश किया जाता है. ऐसा तब तक किया जाता है, जब तक समस्या का सबसे सही समाधान नहीं मिल जाता.
अनुमान
मान लें कि आपके पास सीपीयू का ज़्यादा इस्तेमाल करने वाला कोई ऐसा टास्क है जिसे आपको WebAssembly (Wasm) को सौंपना है, ताकि वह नेटिव परफ़ॉर्मेंस के करीब काम कर सके. इस गाइड में, सीपीयू का ज़्यादा इस्तेमाल करने वाले टास्क का उदाहरण दिया गया है. यह टास्क, किसी संख्या के फ़ैक्टोरियल का हिसाब लगाता है. फ़ैक्टोरियल, किसी पूर्णांक और उससे कम के सभी पूर्णांकों का गुणनफल होता है. उदाहरण के लिए, चार का फ़ैक्टोरियल (4!
के तौर पर लिखा जाता है) 24
के बराबर होता है. इसका मतलब है कि 4 * 3 * 2 * 1
. संख्याएं तेज़ी से बढ़ती हैं. उदाहरण के लिए, 16!
, 2,004,189,184
है. सीपीयू का ज़्यादा इस्तेमाल करने वाले टास्क का एक और उदाहरण, बारकोड स्कैन करना या रास्टर इमेज को ट्रेस करना हो सकता है.
नीचे दिए गए C++ में लिखे गए कोड के सैंपल में, factorial()
फ़ंक्शन को बार-बार लागू करने का तरीका दिखाया गया है. इसमें रिकर्सिव के बजाय, बार-बार लागू होने वाले तरीके का इस्तेमाल किया गया है.
#include <stdint.h>
extern "C" {
// Calculates the factorial of a non-negative integer n.
uint64_t factorial(unsigned int n) {
uint64_t result = 1;
for (unsigned int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
}
इस लेख के बाकी हिस्से के लिए, मान लें कि factorial.wasm
फ़ंक्शन को Emscripten के साथ कंपाइल करके, factorial.wasm
नाम की फ़ाइल में Wasm मॉड्यूल बनाया गया है. इसमें कोड ऑप्टिमाइज़ेशन के सभी सबसे सही तरीके इस्तेमाल किए गए हैं.factorial()
इसे करने के तरीके के बारे में फिर से जानने के लिए, ccall/cwrap का इस्तेमाल करके, JavaScript से कंपाइल किए गए C फ़ंक्शन को कॉल करना लेख पढ़ें.
factorial.wasm
को स्टैंडअलोन Wasm के तौर पर कंपाइल करने के लिए, इस निर्देश का इस्तेमाल किया गया था.
emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]' --no-entry
एचटीएमएल में, form
है. इसमें input
को output
और सबमिट button
के साथ जोड़ा गया है. इन एलिमेंट को JavaScript में उनके नाम के आधार पर रेफ़रंस किया जाता है.
<form>
<label>The factorial of <input type="text" value="12" /></label> is
<output>479001600</output>.
<button type="submit">Calculate</button>
</form>
const input = document.querySelector('input');
const output = document.querySelector('output');
const button = document.querySelector('button');
मॉड्यूल को लोड करने, कंपाइल करने, और इंस्टैंशिएट करने में लगने वाला समय
किसी Wasm मॉड्यूल का इस्तेमाल करने से पहले, आपको उसे लोड करना होगा. वेब पर, ऐसा fetch()
एपीआई के ज़रिए होता है. जैसा कि आपको पता है कि आपका वेब ऐप्लिकेशन, सीपीयू का ज़्यादा इस्तेमाल करने वाले टास्क के लिए Wasm मॉड्यूल पर निर्भर करता है. इसलिए, आपको Wasm फ़ाइल को जल्द से जल्द प्रीलोड करना चाहिए. इसके लिए, आपको अपने ऐप्लिकेशन के <head>
सेक्शन में, सीओआरएस की सुविधा वाले फ़ेच का इस्तेमाल करना होगा.
<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />
असल में, fetch()
एपीआई एसिंक्रोनस है और आपको नतीजे का await
करना होगा.
fetch('factorial.wasm');
इसके बाद, Wasm मॉड्यूल को कंपाइल और इंस्टैंटिएट करें. इन टास्क के लिए, WebAssembly.compile()
(प्लस WebAssembly.compileStreaming()
) और WebAssembly.instantiate()
नाम के फ़ंक्शन उपलब्ध हैं. हालांकि, WebAssembly.instantiateStreaming()
तरीके से, Wasm मॉड्यूल को सीधे तौर पर स्ट्रीम किए गए सोर्स, जैसे कि fetch()
से कंपाइल और इंस्टैंशिएट किया जाता है. इसके लिए, await
की ज़रूरत नहीं होती. Wasm कोड को लोड करने का यह सबसे असरदार और ऑप्टिमाइज़ किया गया तरीका है. मान लें कि Wasm मॉड्यूल, factorial()
फ़ंक्शन एक्सपोर्ट करता है. ऐसे में, इसका तुरंत इस्तेमाल किया जा सकता है.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
const factorial = resultObject.instance.exports.factorial;
button.addEventListener('click', (e) => {
e.preventDefault();
output.textContent = factorial(parseInt(input.value, 10));
});
टास्क को वेब वर्कर पर ट्रांसफ़र करना
अगर इस कोड को मुख्य थ्रेड पर चलाया जाता है, तो सीपीयू पर ज़्यादा लोड डालने वाले टास्क की वजह से, पूरा ऐप्लिकेशन ब्लॉक हो सकता है. आम तौर पर, ऐसे टास्क को वेब वर्कर पर ट्रांसफ़र किया जाता है.
मुख्य थ्रेड को फिर से व्यवस्थित किया गया
सीपीयू का ज़्यादा इस्तेमाल करने वाले टास्क को वेब वर्कर पर ले जाने के लिए, पहला चरण ऐप्लिकेशन को फिर से व्यवस्थित करना है. अब मुख्य थ्रेड, Worker
बनाता है. इसके अलावा, यह सिर्फ़ वेब वर्कर को इनपुट भेजने, फिर आउटपुट पाने, और उसे दिखाने का काम करता है.
/* Main thread. */
let worker = null;
// When the button is clicked, submit the input value
// to the Web Worker.
button.addEventListener('click', (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker('worker.js');
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({ integer: parseInt(input.value, 10) });
});
खराब: टास्क, वेब वर्कर में चलता है, लेकिन कोड में रेस की स्थिति है
वेब वर्कर, Wasm मॉड्यूल को इंस्टैंशिएट करता है. साथ ही, मैसेज मिलने पर सीपीयू का इस्तेमाल करने वाला टास्क पूरा करता है और नतीजे को मुख्य थ्रेड पर वापस भेजता है.
इस तरीके में समस्या यह है कि WebAssembly.instantiateStreaming()
के साथ Wasm मॉड्यूल को इंस्टैंशिएट करना, एसिंक्रोनस ऑपरेशन है. इसका मतलब है कि कोड रेसी है. सबसे खराब स्थिति में, मुख्य थ्रेड तब डेटा भेजता है, जब वेब वर्कर तैयार नहीं होता है. साथ ही, वेब वर्कर को कभी भी मैसेज नहीं मिलता है.
/* Worker thread. */
// Instantiate the Wasm module.
// 🚫 This code is racy! If a message comes in while
// the promise is still being awaited, it's lost.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
const factorial = resultObject.instance.exports.factorial;
// Listen for incoming messages, run the task,
// and post the result.
self.addEventListener('message', (e) => {
const { integer } = e.data;
self.postMessage({ result: factorial(integer) });
});
बेहतर: टास्क वेब वर्कर में चलता है, लेकिन इसमें लोड करने और कंपाइल करने की प्रोसेस दोहराई जा सकती है
Wasm मॉड्यूल को एसिंक्रोनस तरीके से इंस्टैंटिएट करने की समस्या को हल करने का एक तरीका यह है कि Wasm मॉड्यूल को लोड करने, कंपाइल करने, और इंस्टैंटिएट करने की प्रोसेस को इवेंट लिसनर में ले जाया जाए. हालांकि, इसका मतलब यह होगा कि यह प्रोसेस हर बार मैसेज मिलने पर करनी होगी. एचटीटीपी कैश मेमोरी में सेव करने की सुविधा और कंपाइल किए गए Wasm बाइटकोड को कैश मेमोरी में सेव करने की सुविधा के साथ, यह सबसे खराब समाधान नहीं है. हालांकि, इससे बेहतर तरीका भी है.
एसिंक्रोनस कोड को वेब वर्कर की शुरुआत में ले जाकर और प्रॉमिस के पूरा होने का इंतज़ार न करके, बल्कि प्रॉमिस को किसी वैरिएबल में सेव करके, प्रोग्राम तुरंत कोड के इवेंट लिसनर वाले हिस्से पर चला जाता है. साथ ही, मुख्य थ्रेड से कोई मैसेज नहीं मिटेगा. इसके बाद, इवेंट लिसनर के अंदर प्रॉमिस का इंतज़ार किया जा सकता है.
/* Worker thread. */
const importObject = {};
// Instantiate the Wasm module.
// 🚫 If the `Worker` is spun up frequently, the loading
// compiling, and instantiating work will happen every time.
const wasmPromise = WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
// Listen for incoming messages
self.addEventListener('message', async (e) => {
const { integer } = e.data;
const resultObject = await wasmPromise;
const factorial = resultObject.instance.exports.factorial;
const result = factorial(integer);
self.postMessage({ result });
});
अच्छा: टास्क, वेब वर्कर में चलता है. साथ ही, यह सिर्फ़ एक बार लोड और कंपाइल होता है
स्टैटिक WebAssembly.compileStreaming()
तरीके का नतीजा एक प्रॉमिस होता है, जो WebAssembly.Module
में बदल जाता है.
इस ऑब्जेक्ट की एक अच्छी सुविधा यह है कि इसे postMessage()
का इस्तेमाल करके ट्रांसफ़र किया जा सकता है.
इसका मतलब है कि Wasm मॉड्यूल को मुख्य थ्रेड में सिर्फ़ एक बार लोड और कंपाइल किया जा सकता है. इसके अलावा, इसे सिर्फ़ लोड और कंपाइल करने वाले किसी अन्य वेब वर्कर में भी लोड और कंपाइल किया जा सकता है. इसके बाद, इसे सीपीयू का ज़्यादा इस्तेमाल करने वाले टास्क के लिए ज़िम्मेदार वेब वर्कर को ट्रांसफ़र किया जा सकता है. नीचे दिए गए कोड में, इस फ़्लो को दिखाया गया है.
/* Main thread. */
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
let worker = null;
// When the button is clicked, submit the input value
// and the Wasm module to the Web Worker.
button.addEventListener('click', async (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker('worker.js');
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
वेब वर्कर के लिए, सिर्फ़ WebAssembly.Module
ऑब्जेक्ट को एक्सट्रैक्ट करना और उसे इंस्टैंशिएट करना बाकी है. WebAssembly.Module
वाला मैसेज स्ट्रीम नहीं किया जाता है. इसलिए, वेब वर्कर में मौजूद कोड अब WebAssembly.instantiate()
का इस्तेमाल करता है. पहले यह instantiateStreaming()
वेरिएंट का इस्तेमाल करता था. इंस्टेंटिएट किए गए मॉड्यूल को किसी वैरिएबल में कैश मेमोरी में सेव किया जाता है. इसलिए, वेब वर्कर को स्पिन अप करने पर, इंस्टैंटिएशन का काम सिर्फ़ एक बार करना होता है.
/* Worker thread. */
let instance = null;
// Listen for incoming messages
self.addEventListener('message', async (e) => {
// Extract the `WebAssembly.Module` from the message.
const { integer, module } = e.data;
const importObject = {};
// Instantiate the Wasm module that came via `postMessage()`.
instance = instance || (await WebAssembly.instantiate(module, importObject));
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({ result });
});
परफ़ेक्ट: टास्क, इनलाइन वेब वर्कर में चलता है. साथ ही, यह सिर्फ़ एक बार लोड और कंपाइल होता है
एचटीटीपी कैश मेमोरी के साथ भी, कैश मेमोरी में सेव किए गए वेब वर्कर कोड को पाना और नेटवर्क पर हिट करना महंगा होता है. परफ़ॉर्मेंस को बेहतर बनाने का एक सामान्य तरीका यह है कि वेब वर्कर को इनलाइन किया जाए और उसे blob:
यूआरएल के तौर पर लोड किया जाए. इसके लिए, कंपाइल किए गए Wasm मॉड्यूल को Web Worker को पास करना ज़रूरी है, ताकि उसे इंस्टैंटिएट किया जा सके. ऐसा इसलिए, क्योंकि Web Worker और मुख्य थ्रेड के कॉन्टेक्स्ट अलग-अलग होते हैं. भले ही, वे एक ही JavaScript सोर्स फ़ाइल पर आधारित हों.
/* Main thread. */
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
let worker = null;
const blobURL = URL.createObjectURL(
new Blob(
[
`
let instance = null;
self.addEventListener('message', async (e) => {
// Extract the \`WebAssembly.Module\` from the message.
const {integer, module} = e.data;
const importObject = {};
// Instantiate the Wasm module that came via \`postMessage()\`.
instance = instance || await WebAssembly.instantiate(module, importObject);
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({result});
});
`,
],
{ type: 'text/javascript' },
),
);
button.addEventListener('click', async (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
लेज़ी या ईगर वेब वर्कर बनाना
अब तक, सभी कोड सैंपल में वेब वर्कर को ज़रूरत के हिसाब से चालू किया गया था. इसका मतलब है कि जब बटन दबाया गया, तब वेब वर्कर चालू हुआ. आपके ऐप्लिकेशन के हिसाब से, Web Worker को ज़्यादा तेज़ी से बनाया जा सकता है. उदाहरण के लिए, जब ऐप्लिकेशन का इस्तेमाल न किया जा रहा हो या ऐप्लिकेशन की बूटस्ट्रैपिंग प्रोसेस के दौरान भी ऐसा किया जा सकता है. इसलिए, Web Worker बनाने वाले कोड को बटन के इवेंट लिसनर से बाहर ले जाएं.
const worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
वेब वर्कर को चालू रखना है या नहीं
आपके मन में यह सवाल आ सकता है कि क्या आपको वेब वर्कर को हमेशा चालू रखना चाहिए या जब इसकी ज़रूरत हो, तब इसे फिर से बनाना चाहिए. दोनों ही तरीकों का इस्तेमाल किया जा सकता है. इनके अपने फ़ायदे और नुकसान हैं. उदाहरण के लिए, Web Worker को हमेशा चालू रखने से, आपके ऐप्लिकेशन की मेमोरी का इस्तेमाल बढ़ सकता है. साथ ही, एक साथ कई टास्क को मैनेज करना मुश्किल हो सकता है. ऐसा इसलिए, क्योंकि आपको Web Worker से मिलने वाले नतीजों को वापस अनुरोधों पर मैप करना होता है. दूसरी ओर, आपके WebWorker का बूटस्ट्रैपिंग कोड काफ़ी जटिल हो सकता है. इसलिए, हर बार नया कोड बनाने पर काफ़ी ओवरहेड हो सकता है. खुशी की बात यह है कि User Timing API का इस्तेमाल करके, इस समस्या का पता लगाया जा सकता है.
अब तक के कोड सैंपल में, एक परमानेंट वेब वर्कर को चालू रखा गया है. यहां दिया गया कोड सैंपल, ज़रूरत पड़ने पर एक नया वेब वर्कर ऐड-हॉक बनाता है. ध्यान दें कि आपको खुद ही वेब वर्कर को बंद करने पर नज़र रखनी होगी. (कोड स्निपेट में गड़बड़ी ठीक करने की सुविधा नहीं है. हालांकि, अगर कोई गड़बड़ी होती है, तो पक्का करें कि सभी मामलों में प्रोसेस बंद हो जाए. भले ही, प्रोसेस पूरी हो गई हो या नहीं.)
/* Main thread. */
let worker = null;
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
const blobURL = URL.createObjectURL(
new Blob(
[
`
// Caching the instance means you can switch between
// throw-away and permanent Web Worker freely.
let instance = null;
self.addEventListener('message', async (e) => {
// Extract the \`WebAssembly.Module\` from the message.
const {integer, module} = e.data;
const importObject = {};
// Instantiate the Wasm module that came via \`postMessage()\`.
instance = instance || await WebAssembly.instantiate(module, importObject);
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({result});
});
`,
],
{ type: 'text/javascript' },
),
);
button.addEventListener('click', async (e) => {
e.preventDefault();
// Terminate a potentially running Web Worker.
if (worker) {
worker.terminate();
}
// Create the Web Worker lazily on-demand.
worker = new Worker(blobURL);
worker.addEventListener('message', (e) => {
worker.terminate();
worker = null;
output.textContent = e.data.result;
});
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
डेमो
आपके पास खेलने के लिए दो डेमो हैं. एक ऐड-हॉक वेब वर्कर (सोर्स कोड) और दूसरा परमानेंट वेब वर्कर (सोर्स कोड).
Chrome DevTools खोलकर कंसोल में, UserTiming API के लॉग देखे जा सकते हैं. इनसे यह पता चलता है कि बटन पर क्लिक करने से लेकर स्क्रीन पर नतीजे दिखने में कितना समय लगा. नेटवर्क टैब में, blob:
यूआरएल
अनुरोध दिखते हैं. इस उदाहरण में, ऐड हॉक और परमानेंट के बीच समय का अंतर करीब तीन गुना है. हालांकि, इस मामले में दोनों को अलग-अलग नहीं किया जा सकता. आपके असल जीवन से जुड़े ऐप्लिकेशन के नतीजे अलग-अलग हो सकते हैं.
मीटिंग में सामने आए नतीजे
इस पोस्ट में, Wasm से जुड़ी परफ़ॉर्मेंस के कुछ पैटर्न के बारे में बताया गया है.
- सामान्य नियम के तौर पर, स्ट्रीमिंग के तरीकों (
WebAssembly.compileStreaming()
औरWebAssembly.instantiateStreaming()
) को बिना स्ट्रीमिंग वाले तरीकों (WebAssembly.compile()
औरWebAssembly.instantiate()
) के मुकाबले प्राथमिकता दें. - अगर हो सके, तो परफ़ॉर्मेंस पर असर डालने वाले टास्क को Web Worker में आउटसोर्स करें. साथ ही, Wasm को लोड करने और कंपाइल करने का काम सिर्फ़ एक बार Web Worker के बाहर करें. इस तरह, Web Worker को सिर्फ़ उस Wasm मॉड्यूल को इंस्टैंशिएट करने की ज़रूरत होती है जो उसे मुख्य थ्रेड से मिलता है. मुख्य थ्रेड में,
WebAssembly.instantiate()
का इस्तेमाल करके मॉड्यूल को लोड और कंपाइल किया जाता है. इसका मतलब है कि अगर Web Worker को हमेशा चालू रखा जाता है, तो इंस्टेंस को कैश मेमोरी में सेव किया जा सकता है. - ध्यान से मेज़र करें कि क्या एक Web Worker को हमेशा चालू रखना सही है या जब भी ज़रूरत हो, तब Web Worker बनाना सही है. यह भी सोचें कि वेब वर्कर बनाने का सबसे सही समय कब है. मेमोरी की खपत, वेब वर्कर को इंस्टैंटिएट करने में लगने वाला समय, और एक साथ कई अनुरोधों को मैनेज करने की जटिलता जैसी बातों का ध्यान रखना ज़रूरी है.
इन पैटर्न को ध्यान में रखने पर, आपको Wasm की सबसे अच्छी परफ़ॉर्मेंस मिल सकती है.
Acknowledgements
इस गाइड की समीक्षा Andreas Haas, Jakob Kummerow, Deepti Gandluri, Alon Zakai, Francis McCabe, François Beaufort, और Rachel Andrew ने की है.