वेब ऐप्लिकेशन के लिए 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 को इस रूप में कंपाइल करने के लिए नीचे दिए गए निर्देश का इस्तेमाल किया गया स्टैंडअलोन 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() एपीआई. जैसा कि आपको पता है कि आपका वेब ऐप्लिकेशन सीपीयू (CPU) का इस्तेमाल करने वाला टास्क, आपको 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 मॉड्यूल को इंस्टैंशिएट करता है और मैसेज मिलने पर, सीपीयू की ज़रूरत से जुड़ा टास्क करता है और नतीजे को वापस मुख्य थ्रेड पर भेजता है. इस तरीके में समस्या यह है कि Wasm मॉड्यूल को इस तरह से इंस्टैंशिएट किया जा रहा है: WebAssembly.instantiateStreaming() एक एसिंक्रोनस कार्रवाई है. इसका मतलब है कि कोड सिर्फ़ वयस्कों के लिए है. सबसे खराब स्थिति में, मुख्य थ्रेड तब डेटा भेजती है, जब वेब कर्मचारी अभी तक तैयार नहीं है और वेब कर्मचारी को कभी भी संदेश प्राप्त नहीं होता है.

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

बेहतर: टास्क, Web Worker में चलता है, लेकिन वह ग़ैर-ज़रूरी लोड और कंपाइल होता है

एसिंक्रोनस Wasm मॉड्यूल इंस्टैंशिएट की समस्या का एक हल यह है कि Wasm मॉड्यूल के लोडिंग, कंपाइलेशन, और इंस्टैंशिएशन को पूरा इवेंट में ले जाएं हालाँकि, इसका मतलब यह होगा कि इस काम को हर मिला मैसेज. एचटीटीपी कैश मेमोरी में सेव होने और एचटीटीपी कैश मेमोरी में सेव की जा सकने वाली Wasm बाइट कोड को कंपाइल नहीं किया गया, यह सबसे खराब समाधान नहीं है, लेकिन तरीका है.

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

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

Web Worker की ओर, बस 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 });
});

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

एचटीटीपी कैशिंग के साथ भी, (आम तौर पर) कैश मेमोरी में सेव किया गया वेब वर्कर कोड पाने और नेटवर्क पर हमला करना महंगा हो सकता है. परफ़ॉर्मेंस की एक आम तरकीब यह है कि Web Worker को इनलाइन करें और उसे 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,
  });
});

लेज़ी या उत्सुक वेब वर्कर बनाना

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

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

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

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

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

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

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

स्वीकार की गई

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