वेब ऐप्लिकेशन के लिए WebAssembly परफ़ॉर्मेंस के पैटर्न

इस गाइड में, उन वेब डेवलपर के लिए जानकारी दी गई है जिन्हें WebAssembly से फ़ायदा लेना है. आपको एक रनिंग उदाहरण की मदद से, सीपीयू से जुड़े टास्क को आउटसोर्स करने के लिए Wasm का इस्तेमाल करने का तरीका बताया जाएगा. इस गाइड में, Wasm मॉड्यूल को लोड करने के सबसे सही तरीकों से लेकर उनके कंपाइलेशन और इंस्टैंशिएशन को ऑप्टिमाइज़ करने तक के बारे में पूरी जानकारी दी गई है. इसमें इसके अलावा, सीपीयू से जुड़े टास्क को वेब वर्कर पर शिफ़्ट करने के बारे में बताया गया है. साथ ही, इसे लागू करने से जुड़े फ़ैसले लिए गए हैं, जैसे कि वेब वर्कर को कब बनाना है और ज़रूरत पड़ने पर इसे हमेशा के लिए चालू रखना है या नहीं करना है. यह गाइड, समस्या को हल करने के तरीके को बार-बार डेवलप करती है और एक बार में एक परफ़ॉर्मेंस पैटर्न बताती है. ऐसा तब तक होता है, जब तक समस्या के सबसे अच्छे समाधान का सुझाव न दिया जाए.

अनुमान

मान लें कि आपके पास सीपीयू की तरह काम करने वाला एक टास्क है, जिसे आपको उनकी क्लोज़-टू-नेटिव परफ़ॉर्मेंस के लिए, WebAssembly (Wasm) से आउटसोर्स करना है. इस गाइड में उदाहरण के तौर पर इस्तेमाल किया गया सीपीयू-इंटेसिव टास्क किसी संख्या के फ़ैक्टोरियल की गणना करता है. क्रमगुणित, किसी पूर्णांक और उसके नीचे मौजूद सभी पूर्णांकों का गुणनफल होता है. उदाहरण के लिए, चार का फ़ैक्टोरियल (4! के रूप में लिखा गया) 24 (यानी, 4 * 3 * 2 * 1) के बराबर होता है. संख्याएं तेज़ी से बड़ी हो जाती हैं. उदाहरण के लिए, 16!, 2,004,189,184 है. बारकोड को स्कैन करना या रास्टर इमेज को ट्रेस करना, सीपीयू-इंटेंसिव टास्क का बेहतर उदाहरण हो सकता है.

factorial() फ़ंक्शन को लागू करने का एक परफ़ॉर्मेंट इटरेटिव (बार-बार होने वाला) फ़ंक्शन C++ में लिखे गए इस कोड सैंपल में दिखाया गया है.

#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;
}

}

बाकी लेख के लिए, मान लें कि एक Wasm मॉड्यूल है, जो कोड ऑप्टिमाइज़ेशन के सबसे सही तरीकों का इस्तेमाल करके, factorial.wasm नाम की फ़ाइल में Emscripten के साथ इस factorial() फ़ंक्शन को कंपाइल करता है. ऐसा करने के तरीके के बारे में ज़्यादा जानने के लिए, ccall/cwrap का इस्तेमाल करके, JavaScript से कंपाइल किए गए C फ़ंक्शन कॉल करना पढ़ें. factorial.wasm को Standalone Wasm के तौर पर कंपाइल करने के लिए, इस कमांड का इस्तेमाल किया गया था.

emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]'  --no-entry

एचटीएमएल में, input के साथ एक form होता है और इसे 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 });
});

अच्छा: टास्क, Web Worker में चलता है और सिर्फ़ एक बार लोड और कंपाइल होता है

स्टैटिक 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 के साथ मैसेज स्ट्रीम नहीं किया गया है, इसलिए वेब वर्कर में यह कोड पहले के instantiateStreaming() वैरिएंट के बजाय, अब WebAssembly.instantiate() का इस्तेमाल करता है. इंस्टैंशिएट किए गए मॉड्यूल को किसी वैरिएबल में कैश मेमोरी में सेव किया जाता है, इसलिए वेब वर्कर को स्पिनिंग के बाद एक बार इंस्टैंशिएट किया जाना ज़रूरी है.

/* 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 मॉड्यूल को इंस्टैंशिएट करने के लिए, वेब वर्कर को भेजना ज़रूरी है. इसकी वजह यह है कि वेब वर्कर और मुख्य थ्रेड के कॉन्टेक्स्ट अलग-अलग होते हैं, भले ही वे एक ही 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,
  });
});

लेज़ी या ईगर वेब वर्कर बनाना

अब तक, सभी कोड सैंपल, वेब वर्कर को ज़रूरत के हिसाब से लेज़ी तरीके से (यानी, बटन दबाने पर) जनरेट करते हैं. आपके ऐप्लिकेशन के आधार पर, वेब वर्कर को ज़्यादा तेज़ी से बनाना सही होगा. उदाहरण के लिए, जब ऐप्लिकेशन कुछ समय से इस्तेमाल में न हो या ऐप्लिकेशन की बूटस्ट्रैपिंग प्रोसेस का हिस्सा हो. इसलिए, वेब वर्कर के क्रिएशन कोड को बटन के इवेंट लिसनर से बाहर ले जाएं.

const worker = new Worker(blobURL);

// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
  output.textContent = e.result;
});

वेब वर्कर को आस-पास रखें या न रखें

खुद से एक सवाल यह पूछा जा सकता है कि क्या आपको वेब वर्कर को स्थायी रूप से वहां रखना चाहिए या ज़रूरत पड़ने पर इसे फिर से बनाना चाहिए. दोनों तरीकों से मदद मिल सकती है. साथ ही, इनके अपने फ़ायदे और नुकसान भी हैं. उदाहरण के लिए, किसी वेब वर्कर को हमेशा के लिए बनाए रखने से आपके ऐप्लिकेशन की मेमोरी फ़ुटप्रिंट बढ़ सकता है और एक साथ किए जाने वाले कामों को पूरा करने में मुश्किल हो सकती है, क्योंकि आपको वेब वर्कर से वापस अनुरोधों पर आने वाले नतीजों को मैप करने की ज़रूरत होती है. दूसरी ओर, आपके वेब वर्कर का बूटस्ट्रैपिंग कोड काफ़ी जटिल हो सकता है. इसलिए, अगर आप हर बार एक नया बनाते हैं, तो बहुत सारे ओवरहेड हो सकते हैं. अच्छी बात यह है कि इसे 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: यूआरएल अनुरोध दिखाता है. इस उदाहरण में, ऐड-हॉक और स्थायी के बीच समय का अंतर करीब 3× है. इस मामले में, मानवीय आंखों के लिए, दोनों को पहचाना नहीं जा सकता. हो सकता है कि आपके असली ऐप्लिकेशन के नतीजे अलग-अलग हों.

ऐड-हॉक वर्कर के साथ फ़ैक्टोरियल Wasm डेमो ऐप्लिकेशन. Chrome DevTools खुला है. दो ब्लॉब होते हैं: नेटवर्क टैब में यूआरएल के अनुरोध और कंसोल में हिसाब लगाने के दो समय दिखाए जाते हैं.

स्थायी कर्मचारी की मदद से बनाया गया फ़ैक्टोरियल Wasm डेमो ऐप्लिकेशन. Chrome DevTools खुला है. इसमें सिर्फ़ एक ब्लॉब मौजूद है: नेटवर्क टैब में यूआरएल अनुरोध और कंसोल में कैलकुलेशन के चार समय दिखाए जाते हैं.

मीटिंग में सामने आए नतीजे

इस पोस्ट में, Wasm के इस्तेमाल से जुड़ी कुछ परफ़ॉर्मेंस के पैटर्न के बारे में बताया गया है.

  • सामान्य नियम के हिसाब से, स्ट्रीमिंग के तरीकों (WebAssembly.compile() और WebAssembly.instantiate()) के बजाय, स्ट्रीमिंग के तरीकों (WebAssembly.compileStreaming() और WebAssembly.instantiateStreaming()) को प्राथमिकता दें.
  • अगर हो सके, तो परफ़ॉर्मेंस से जुड़े भारी कामों को वेब वर्कर में आउटसोर्स करें और Wasm लोडिंग और कंपाइल करने का काम सिर्फ़ एक बार वेब वर्कर से बाहर करें. इस तरह, वेब वर्कर को सिर्फ़ मुख्य थ्रेड से मिलने वाले Wasm मॉड्यूल को इंस्टैंशिएट करने की ज़रूरत होती है, जहां WebAssembly.instantiate() के साथ लोडिंग और कंपाइलिंग होती है. इसका मतलब है कि अगर वेब वर्कर को हमेशा के लिए इस्तेमाल किया जाता है, तो इंस्टेंस को कैश मेमोरी में सेव किया जा सकता है.
  • इस बात पर ध्यान से आकलन करें कि एक स्थायी वेब वर्कर को हमेशा के लिए रखना सही है या ज़रूरत पड़ने पर अतिरिक्त वेब वर्कर बनाना. साथ ही, यह भी सोचें कि वेब वर्कर बनाने का सबसे सही समय कौनसा होगा. इन बातों पर ध्यान दें: मेमोरी का इस्तेमाल करना और वेब वर्कर के इंस्टैंशिएट करने की अवधि. हालांकि, एक साथ मिलने वाले अनुरोधों को मैनेज करने में लगने वाली दिक्कतें भी हैं.

अगर इन पैटर्न को ध्यान में रखा जाए, तो इसका मतलब है कि Google की खराब परफ़ॉर्मेंस की परफ़ॉर्मेंस बेहतर हो सकती है.

स्वीकार हैं

इस गाइड की समीक्षा ऐंड्रियास हास, जेकब कुमरोव, दीप्ती गैंडलुरी, एलॉन ज़काई, फ़्रैंसिस मैककैब, फ़्रैंसुआ बूफ़ोर्ट, और रेचल एंड्रू ने की है.