এই নির্দেশিকাটিতে, ওয়েব ডেভেলপারদের উদ্দেশ্যে যারা WebAssembly থেকে উপকৃত হতে চান, আপনি একটি চলমান উদাহরণের সাহায্যে CPU- নিবিড় কাজগুলিকে আউটসোর্স করতে Wasm ব্যবহার করতে শিখবেন। গাইডটি Wasm মডিউল লোড করার সর্বোত্তম অনুশীলন থেকে শুরু করে তাদের সংকলন এবং ইনস্ট্যান্টিয়েশন অপ্টিমাইজ করা পর্যন্ত সবকিছু কভার করে। এটি সিপিইউ-নিবিড় কাজগুলিকে ওয়েব ওয়ার্কারদের কাছে স্থানান্তরিত করার বিষয়ে আরও আলোচনা করে এবং কখন ওয়েব ওয়ার্কার তৈরি করতে হবে এবং এটিকে স্থায়ীভাবে জীবিত রাখতে হবে বা প্রয়োজনের সময় এটিকে স্পিন করতে হবে কিনা সেই মত বাস্তবায়নের সিদ্ধান্তগুলির দিকে নজর দেয়। গাইডটি পুনরাবৃত্তিমূলকভাবে পদ্ধতির বিকাশ করে এবং সমস্যাটির সর্বোত্তম সমাধানের পরামর্শ না দেওয়া পর্যন্ত একবারে একটি কর্মক্ষমতা প্যাটার্ন প্রবর্তন করে।
অনুমান
ধরে নিন আপনার কাছে একটি খুব সিপিইউ-নিবিড় টাস্ক রয়েছে যা আপনি এর কাছাকাছি থেকে নেটিভ পারফরম্যান্সের জন্য WebAssembly (Wasm) এ আউটসোর্স করতে চান। এই নির্দেশিকায় উদাহরণ হিসেবে ব্যবহৃত CPU- নিবিড় কাজটি একটি সংখ্যার ফ্যাক্টরিয়াল গণনা করে। ফ্যাক্টরিয়াল হল একটি পূর্ণসংখ্যার গুণফল এবং তার নীচের সমস্ত পূর্ণসংখ্যা। উদাহরণস্বরূপ, চারটির ফ্যাক্টরিয়াল ( 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;
}
}
বাকি নিবন্ধের জন্য, অনুমান করুন যে সমস্ত কোড অপ্টিমাইজেশান সর্বোত্তম অনুশীলন ব্যবহার করে factorial.wasm
নামে একটি ফাইলে Emscripten-এর সাথে এই factorial()
ফাংশনটি কম্পাইল করার উপর ভিত্তি করে একটি Wasm মডিউল রয়েছে। কিভাবে এটি করতে হয় তার রিফ্রেশারের জন্য, ccall/cwrap ব্যবহার করে JavaScript থেকে কলিং কম্পাইল করা C ফাংশন পড়ুন। নিম্নলিখিত কমান্ডটি factorial.wasm
স্বতন্ত্র Wasm হিসাবে কম্পাইল করতে ব্যবহৃত হয়েছিল।
emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]' --no-entry
HTML-এ, একটি input
সহ একটি form
আছে যা একটি output
এবং একটি জমা button
সাথে যুক্ত। এই উপাদানগুলি তাদের নামের উপর ভিত্তি করে জাভাস্ক্রিপ্ট থেকে উল্লেখ করা হয়।
<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()
API-এর মাধ্যমে ঘটে। যেহেতু আপনি জানেন যে আপনার ওয়েব অ্যাপটি CPU- নিবিড় টাস্কের জন্য Wasm মডিউলের উপর নির্ভর করে, আপনার Wasm ফাইলটি যত তাড়াতাড়ি সম্ভব প্রিলোড করা উচিত। আপনি আপনার অ্যাপের <head>
বিভাগে একটি CORS-সক্ষম আনার মাধ্যমে এটি করেন।
<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />
বাস্তবে, fetch()
API অসিঙ্ক্রোনাস এবং আপনাকে ফলাফলের জন্য 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));
});
কাজটি একজন ওয়েব ওয়ার্কারের কাছে স্থানান্তর করুন
আপনি যদি এটিকে মূল থ্রেডে কার্যকর করেন, সত্যিকারের CPU- নিবিড় কাজগুলির সাথে, আপনি পুরো অ্যাপটিকে ব্লক করার ঝুঁকি নিয়ে থাকেন। একটি সাধারণ অভ্যাস হল এই ধরনের কাজগুলিকে ওয়েব ওয়ার্কারের কাছে স্থানান্তর করা।
প্রধান থ্রেড পুনর্গঠন
সিপিইউ-নিবিড় কাজটিকে ওয়েব ওয়ার্কারে স্থানান্তর করতে, প্রথম পদক্ষেপটি হল অ্যাপ্লিকেশনটিকে পুনর্গঠন করা। মূল থ্রেডটি এখন একটি 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) });
});
খারাপ: টাস্ক ওয়েব ওয়ার্কারে চলে, কিন্তু কোডটি খুব কম
ওয়েব ওয়ার্কার ওয়াসম মডিউলটি ইনস্ট্যান্টিয়েট করে এবং একটি বার্তা পাওয়ার পরে, সিপিইউ-নিবিড় কাজটি সম্পাদন করে এবং ফলাফলটি মূল থ্রেডে ফেরত পাঠায়। এই পদ্ধতির সমস্যা হল 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 মডিউল লোডিং, সংকলন এবং ইন্সট্যান্টিয়েশন সবই ইভেন্ট লিসেনারে নিয়ে যাওয়া, কিন্তু এর মানে এই যে এই কাজটি প্রতিটি প্রাপ্ত বার্তায় ঘটতে হবে। এইচটিটিপি ক্যাশিং এবং এইচটিটিপি ক্যাশে কম্পাইল করা ওয়াসম বাইটকোড ক্যাশে করতে সক্ষম, এটি সবচেয়ে খারাপ সমাধান নয়, তবে আরও ভাল উপায় রয়েছে।
অ্যাসিঙ্ক্রোনাস কোডটিকে ওয়েব ওয়ার্কারের শুরুতে স্থানান্তরিত করে এবং প্রতিশ্রুতি পূরণের জন্য অপেক্ষা না করে, বরং প্রতিশ্রুতিটিকে একটি ভেরিয়েবলে সংরক্ষণ করে, প্রোগ্রামটি অবিলম্বে কোডের ইভেন্ট শ্রোতা অংশে চলে যায় এবং এর থেকে কোনও বার্তা আসে না। মূল থ্রেড হারিয়ে যাবে। অনুষ্ঠান শ্রোতার ভিতরে, প্রতিশ্রুতি তারপর অপেক্ষা করা যেতে পারে.
/* 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()
ব্যবহার করে স্থানান্তর করা যেতে পারে। এর মানে হল ওয়াসম মডিউলটি মূল থ্রেডে একবার লোড এবং কম্পাইল করা যেতে পারে (অথবা এমনকি অন্য ওয়েব ওয়ার্কার লোডিং এবং কম্পাইলিংয়ের সাথে সম্পূর্ণভাবে সংশ্লিষ্ট), এবং তারপরে CPU-নিবিড় কাজের জন্য দায়ী ওয়েব ওয়ার্কারের কাছে স্থানান্তরিত করা যেতে পারে। নিম্নলিখিত কোড এই প্রবাহ দেখায়.
/* 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 });
});
পারফেক্ট: টাস্ক ইনলাইন ওয়েব ওয়ার্কারে চলে এবং শুধুমাত্র একবার লোড ও কম্পাইল হয়
এমনকি HTTP ক্যাশিং সহ, (আদর্শভাবে) ক্যাশ করা ওয়েব ওয়ার্কার কোড প্রাপ্ত করা এবং সম্ভাব্যভাবে নেটওয়ার্কে আঘাত করা ব্যয়বহুল। একটি সাধারণ কর্মক্ষমতা কৌশল হল ওয়েব ওয়ার্কারকে ইনলাইন করা এবং এটিকে একটি blob:
URL হিসাবে লোড করা। এটির জন্য এখনও কম্পাইল করা Wasm মডিউলটি ওয়েব ওয়ার্কারকে ইনস্ট্যান্টেশনের জন্য পাস করতে হবে, কারণ ওয়েব ওয়ার্কার এবং মূল থ্রেডের প্রসঙ্গ ভিন্ন, এমনকি যদি তারা একই জাভাস্ক্রিপ্ট সোর্স ফাইলের উপর ভিত্তি করে থাকে।
/* 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;
});
ওয়েব ওয়ার্কারকে আশেপাশে রাখুন বা না রাখুন
একটি প্রশ্ন যা আপনি নিজেকে জিজ্ঞাসা করতে পারেন তা হল আপনার ওয়েব ওয়ার্কারকে স্থায়ীভাবে রাখা উচিত, নাকি যখনই আপনার প্রয়োজন হবে তখনই এটি পুনরায় তৈরি করা উচিত। উভয় পন্থা সম্ভব এবং তাদের সুবিধা এবং অসুবিধা আছে। উদাহরণস্বরূপ, একজন ওয়েব ওয়ার্কারকে স্থায়ীভাবে আশেপাশে রাখলে আপনার অ্যাপের মেমরির পদচিহ্ন বাড়তে পারে এবং সমসাময়িক কাজগুলিকে আরও কঠিন করে তুলতে পারে, যেহেতু আপনাকে ওয়েব ওয়ার্কার থেকে অনুরোধে ফিরে আসা ফলাফলগুলিকে ম্যাপ করতে হবে৷ অন্যদিকে, আপনার ওয়েব ওয়ার্কারের বুটস্ট্র্যাপিং কোডটি জটিল হতে পারে, তাই আপনি যদি প্রতিবার একটি নতুন তৈরি করেন তাহলে প্রচুর ওভারহেড হতে পারে। ভাগ্যক্রমে এটি এমন কিছু যা আপনি ব্যবহারকারী টাইমিং 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 ওপেন করেন এবং কনসোল চেক করেন, আপনি ব্যবহারকারী টাইমিং API লগগুলি দেখতে পাবেন যা স্ক্রিনে প্রদর্শিত ফলাফলে বোতাম ক্লিক থেকে যে সময় নেয় তা পরিমাপ করে৷ নেটওয়ার্ক ট্যাবটি blob:
URL অনুরোধ(গুলি)৷ এই উদাহরণে, অ্যাডহক এবং স্থায়ী মধ্যে সময়ের পার্থক্য প্রায় 3×। বাস্তবে, মানুষের চোখের কাছে, উভয়ই এই ক্ষেত্রে আলাদা নয়। আপনার নিজের বাস্তব জীবনের অ্যাপের ফলাফল সম্ভবত পরিবর্তিত হবে।
উপসংহার
এই পোস্টটি Wasm এর সাথে ডিল করার জন্য কিছু পারফরম্যান্স প্যাটার্ন অন্বেষণ করেছে।
- একটি সাধারণ নিয়ম হিসাবে, স্ট্রিমিং পদ্ধতিগুলি (
WebAssembly.compileStreaming()
এবংWebAssembly.instantiateStreaming()
)কে তাদের নন-স্ট্রিমিং প্রতিরূপ (WebAssembly.compile()
এবংWebAssembly.instantiate()
) এর চেয়ে বেশি পছন্দ করুন। - আপনি যদি পারেন, একজন ওয়েব ওয়ার্কারে পারফরম্যান্স-ভারী কাজগুলি আউটসোর্স করুন এবং ওয়েব ওয়ার্কারের বাইরে শুধুমাত্র একবার ওয়াসম লোডিং এবং কম্পাইলিং কাজ করুন৷ এইভাবে, ওয়েব ওয়ার্কারকে শুধুমাত্র প্রধান থ্রেড থেকে প্রাপ্ত Wasm মডিউলটি ইনস্ট্যান্সিয়েট করতে হবে যেখানে
WebAssembly.instantiate()
এর সাথে লোডিং এবং কম্পাইল করা হয়েছে, যার মানে আপনি যদি ওয়েব ওয়ার্কারকে স্থায়ীভাবে রাখেন তাহলে উদাহরণটি ক্যাশে করা যেতে পারে। - একজন স্থায়ী ওয়েব কর্মীকে চিরকালের জন্য রাখা বা অ্যাডহক ওয়েব ওয়ার্কারদের যখনই প্রয়োজন হবে তখন তৈরি করা অর্থপূর্ণ কিনা তা সাবধানে পরিমাপ করুন৷ ওয়েব ওয়ার্কার তৈরি করার সেরা সময় কখন তা ভাবুন। যে বিষয়গুলি বিবেচনায় নেওয়া উচিত তা হল মেমরি খরচ, ওয়েব ওয়ার্কার ইনস্ট্যান্টেশনের সময়কাল, তবে সম্ভবত সমকালীন অনুরোধগুলি মোকাবেলা করার জটিলতাও।
আপনি যদি এই প্যাটার্নগুলিকে বিবেচনায় নেন, আপনি সর্বোত্তম Wasm পারফরম্যান্সের জন্য সঠিক পথে আছেন।
স্বীকৃতি
এই নির্দেশিকাটি আন্দ্রেয়াস হাস , জ্যাকব কুমেরো , দীপ্তি গ্যান্ডলুরি , অ্যালন জাকাই , ফ্রান্সিস ম্যাককেবে , ফ্রাঙ্কোইস বিউফোর্ট এবং রাচেল অ্যান্ড্রু দ্বারা পর্যালোচনা করা হয়েছে৷