استفاده از Web Assembly برای سرعت بخشی به اپلیکیشن‌های Angular
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 10 دقیقه

استفاده از Web Assembly برای سرعت بخشی به اپلیکیشن‌های Angular

برای برخی از اپلیکیشن‌ها جاوااسکریپت به اندازه کافی کامل نیست، اما امیدی وجود دارد. Web Assembly از جاوااسکریپت سریع‌تر است و می‌توانید آن را در بیشتر مرورگرها اجرا کنید. در این آموزش قرار است نگاهی به Web Assembly در Angular بیاندازیم.

ما قصد داریم شیوه کامپایل کردن برنامه‌های C را در Web Assembly و استفاده از آن در یک سرویس ساده Angular را برای سرعت‌دهی بیشتر یاد بگیریم. 

Web Assembly چیست؟

Web Assembly یک قالب دستورالعملی دودویی است. از آن‌جایی که از قالب دودویی استفاده می‌کند بنابراین بسیار کوچک طراحی شده است. مزیت اصلی Web Assembly سریع بودن آن است. تقریبا درست به اندازه یک برنامه محلی سریع است. اما این موضوع چرا اهمیت دارد؟

جاوااسکریپت کند است!

در مقایسه با برنامه‌های نوشته شده با C و C++ جاوااسکریپت بسیار کند است. این بدان دلیل است که جاوااسکریپت به صورت مستقیم به دستورالعملات پردازنده تبدیل نمی‌شود، بلکه در محیط ماشین مجازی  اجرا می‌شود. این موضوع به توسعه‌دهنده امکانات بسیار خوبی مانند خودکارسازی‌های مقداردهی به حافظه و... را می‌دهد. پس توسعه‌دهنده نیازی ندارد که درگیر تخصیص دادن حافظه و موارد مربوطه به اپلیکیشن باشد. همانطور که گذشته نشان می‌دهد این پروسه می‌تواند بسیار پیچیده و وقت‌گیر باشد، همچنین احتمال خطا کردن در آن بسیار زیاد است. بنابراین درگیر نبودن با این بخش از برنامه حقیقتا چیز خوبی است.

اما این تسهیلات هزینه‌هایی نیز دارد. اضافه کردن یک لایه اضافی در ماشین مجازی باعث می‌شود که کارایی اپلیکیشن‌های جاوااسکریپتی ضعیف شود. بیشتر از آن، جاوااسکریپت یک زبان کامپایلی نیست. بنابراین نیاز است که در یک runtime اجرا شده کامپایل شود. به دلیل اینکه پروسه کامپایل کردن در این حالت بسیار کند است جاوااسکریپت از یک تکنیک به نام JIT استفاده می‌کند که مخفف Just-In-Time است. با وجود آنکه JIT زمان بارگذاری برنامه را کاهش می‌دهد اما به اندازه برنامه‌های از پیش کامپایل شده سریع نیست.

پیش به سوی Web Assembly برای نجات یافتن!

جدی می‌گویم، جاوااسکریپت برای بیشتر وب اپلیکیشن‌ها مناسب است. استفاده از آن ساده است و با وجود فریمورک‌های مدرن توسعه در آن بسیار سریع است. اما برای اپلیکیشن‌های منحصر به فردی سریع نیست. منظور منحصر به فرد اپلیکیشن‌هایی است که در آن‌ها محاسبات زیادی صورت می‌گیرد. اینجا جائیست که Web Assembly خود را نشان می‌دهد. مثالی از این اپلیکیشن‌ها را می‌توانید برنامه‌های ویرایش تصویر، ساخت بازی و... نام برد.

اولین پیشنویس Web Assembly

نسخه کنونی Web Assembly نسبت به ویژگی‌هایی که قول به ارائه آن‌ها داده شده است بسیار محدود است. در حقیقت این نسخه تنها یک پیش‌نویس برای تاسیس کردن یک تکنولوژی جدید و به هیجان در آوردن مردم در رابطه با آن است. 

با وجود آنکه مدت زیادی از عرضه آن گذشته اما Web Assembly در مرورگرهای FireFox، Chrome، Safari و Microsoft Edge پشتیبانی می‌شود.

یک برنامه C در Angular

Web Assembly می‌تواند از طریق زبان‌های مختلفی مانند C، C++ و Rust تولید شود. حتی یک پروژه آزمایشی نیز وجود دارد که Typescript را به Web Assembly تبدیل می کند. برای این مطلب ما همه چیز را ساده نگه می‌داریم و از یک برنامه ساده C استفاده می‌کنیم.

پیاده‌سازی یک پروژه جدید Angular

قبل از اینکه شروع کنیم، نیاز است که یک پروژه جدید را پیاده‌سازی نماییم. برای اینکار ما از angular-cli استفاده می‌کنیم:

ٰ

ng new angular-wasm

همچنین به Typeهای مختلف برای Web Assembly Javascript API نیاز داریم:

npm install @types/webassembly-js-api --dev --save

نصب کردن کامپایلر Web Assembly

برای نصب کردن کامپایلر Web Assembly می‌توانید این مستندات را مطالعه کنید. 

نکته: اگر از ویندوز استفاده می‌کنید نیاز است که مسیر emsdk را به متغیرهای PATH اضافه کنید.

web assembly در انگولار

کامپایل کردن C به WASM

قبل از اینکه هرچیزی را کامپایل کنیم، نیاز است که یک برنامه C را ایجاد نماییم. برای انجام چنین کاری لطفا پوشه‌ای با نام wasm در دایرکتوری روت پروژه‌تان ایجاد کنید. در داخل آن یک فایل با نام fibonacci.c را ایجاد کنید. 

درست است! مثال ما قرار است که اعداد فیبوناچی برای هر عددی را حساب کند. چنین مثالی را می‌توان تقریبا مثالی با بار محاسباتی بالا دانست مخصوصا اگر عدد بالایی باشد.

برنامه C

برنامه C چیزی شبیه به زیر خواهد بود:

int fibonacci(int n)
{
    if (n == 0 || n == 1)
        return n;
    else
        return (fibonacci(n - 1) + fibonacci(n - 2));
}

حتی اگر با برنامه‌نویسی c آشنایی نداشته باشید مطمئنا درک این برنامه برای‌تان سخت نخواهد بود.

قرار دادن تابع در جاوااسکریپت

حال کاری که قصد انجام آن را داریم این است که تابع فیبوناچی را برای کدهای جاوااسکریپت قابل فراخوانی کنیم. بنابراین نیاز است که آن را به عنوان یک دکوراتور خاص برچسب بزنیم. برای انجام چنین کاری برنامه را به صورت زیر تغییر دهید:

#include <emscripten.h>

int EMSCRIPTEN_KEEPALIVE fibonacci(int n)
{
    if (n == 0 || n == 1)
        return n;
    else
        return (fibonacci(n - 1) + fibonacci(n - 2));
}

کامپایل کردن C به Web Assembly 

برای استفاده از تابع نیاز است که آن را ابتدا به Web Assembly کامپایل کنیم، به این دلیل که مرورگر زبان C را متوجه نمی‌شود. برای انجام این کار از کامپایلری که قبلا نصب کرده‌ایم استفاده می‌کنیم. دستور کامپایل به شکل زیر خواهد بود:

emcc wasm/fibonacci.c -Os -s WASM=1 -s MODULARIZE=1 -o wasm/fibonacci.js

گزینه -Os درجه بهینه‌سازی‌های انجام شده را معین می‌کند. ما از یک درجه بالا استفاده می‌کنیم. 

علاوه بر این کامپایلر یک فایل جاوااسکریپتی را نیز ایجاد می‌کند. در این فایل یکسری کدها وجود دارد که ارتباط بین WASM و Javascript را مدیریت می‌کند. با استفاده از گزینه MODULARIZE=1 ما به کامپایلر خواهیم گفت که کدها را در یک ماژول قرار دهد. با این کار استفاده از اپلیکیشن در پروژه Angular بسیار آسان‌تر می‌شود.

نتیجه کامپایل بالا در نهایت باید دو فایل باشد: fibonacci.js و fibonacci.wasm.

web assembly در انگولار

قراردادن Web Assembly در یک سرویس Angular

حال می‌توانیم از تابع WASM در انگولار استفاده کنیم. بهترین راه برای استفاده کردن از این تابع، قرار دادن آن در یک سرویس جداگانه است. بنابراین ما یک سرویس جدید با نام WasmService را ایجاد می‌کنیم. 

برای اینکه از طریق angular-cli این سرویس را ایجاد کنیم، به صورت زیر عمل می‌نماییم:

ng generate service wasm

متاسفانه استفاده کردن از ماژول‌های WASM به اندازه استفاده کردن جاوااسکریپت ساده نیست. در اینجا ما فقط ماژول را import نمی‌کنیم:

import * as Module from './../../wasm/fibonacci.js';

بلکه باید خود فایل را نیز از طریق file-loader انتقال دهیم.

import '!!file-loader?name=wasm/fibonacci.wasm!../../wasm/fibonacci.wasm';

ما همچنین نیاز داریم که متغیر WebAssembly را تعریف کنیم، در  غیر اینصورت AOT-build به درستی کار نمی‌کند. بنابراین این مورد را در بالای فایل سرویس‌ها اضافه می‌کنیم:

declare var WebAssembly;

نمونه اولیه Web Assembly

برای استفاده از فایل WASM در runtime نیاز است که فایل از طریق URL دریافت شده و به یک آرایه بایتی تبدیل شود. 

برای انجام این، ما یک متد جدید را در سرویس‌مان با نام instantiateWasm ایجاد می‌کنیم:

private async instantiateWasm(url: string){

  // fetch the wasm file

  const wasmFile = await fetch(url);

  // convert it into a binary array

  const buffer = await wasmFile.arrayBuffer();

  const binary = new Uint8Array(buffer);

  // create module arguments 

  // including the wasm-file

  const moduleArgs = {

    wasmBinary: binary,

    onRuntimeInitialized: () => {

      // TODO

    }

  };

  // instantiate the module

  this.module = Module(moduleArgs);

}

یادآوری کنم که ما به یک خاصیت نیز در سرویس به نام module نیاز داریم. این خاصیت شامل ماژول، همراه با تمام توابعی که شامل آن می‌شود، است. حال می‌توانیم متد را در سازنده مربوط به سرویس‌مان فراخوانی کنیم:

constructor() {

  this.instantiateWasm('wasm/fibonacci.wasm');

}

برای اینکه تابع فیبوناچی را در کامپوننت Angular ایجاد کنیم یک متد را به همان نام در سرویس‌مان ایجاد می‌کنیم. در داخل آن متد ما تابع WASM را فراخوانی می‌کنیم.

public fibonacci(input: number): number{

 return this.module._fibonacci(input)

}

اگر توجه کنید متوجه می‌شوید که تمام توابع که در حقیقت توابع WASM هستند با یک آندرسکور شروع می‌شوند.

تاخیر در اجرای تابع تا زمان بارگذاری کامل Web Assembly

این هم از این! حال می‌توانیم از این سرویس درست مانند دیگر سرویس‌های انگولار استفاده کنیم. اما یک مشکل وجود دارد. 

متد instantiateWasm در حقیقت async است و زمانی را برای بارگذاری ماژول Web Assembly نیاز دارد. زمانی که فردی متد Fibonacci ما را فراخوانی کند، خاصیت ماژول نامعین است. 

برای حل این مشکل ما متد فیبوناچی‌مان را برای برگشت یک observable تغییر می‌دهیم. با کمک observable می‌توانیم در اجرای متد تا زمانی که سرویس کاملا آماده است تاخیر بیاندازیم. بیشتر از این نیاز است که یک BehaviorSubject را در سرویس‌مان ایجاد کنیم.

wasmReady = new BehaviorSubject<boolean>(false);

ما این Subject را زمانی که سرویس‌مان آماده است بروزرسانی می‌کنیم:

// update this in instantiateWasm()

const moduleArgs = {

  wasmBinary: binary,

  onRuntimeInitialized: () => {

    this.wasmReady.next(true); // <-- this line

  }

};

بعد از آن observable در متد فیبوناچی را تنها تا زمانی که مقدار Subject برابر با True است فیلتر می‌کنیم. برای اینکار از متد filter در rxjs استفاده می‌کنیم. 

import { filter} from 'rxjs/operators';

در نهایت pipeline نهایی در فایل wasam.service.ts به صورت زیر است:

public fibonacci(input: number): Observable<number> {

    return this.wasmReady.pipe(filter(value => value === true)).pipe(

      map(() => {

        return this.module._fibonacci(input);

      })

    );

  }

رابط کاربری را واکنشگرا نگه‌دارید

رابط کاربری را واکنشگرا نگه‌دارید

مثال بالای فیبوناچی به نظر می‌رسد که در حالت blocking به سر می‌برد. با این حال رابط کاربری تا زمانی که متدها کامل نشوند منجمد می‌مانند. 

برای حل کردن این مشکل، نیاز است که متد غیرهمزمانی را با استفاده از setTimeout اجرا کنیم. برای انجام چنین کاری ما یک observable جدید را از یک promise ایجاد می‌کنیم. این promise وقتی متد غیرهمزمانی کامل شود، تکمیل می‌شود.

public fibonacci(input: number): Observable<number> {

  return this.wasmReady.pipe(filter(value => value === true)).pipe(

    mergeMap(() => {

      return fromPromise(

        new Promise<number>((resolve, reject) => {

          setTimeout(() => {

            const result = this.module._fibonacci(input);

            resolve(result);

          });

        })

      );

    }),

    take(1)

  );

}

اگر راهکار بهتری سراغ دارید برای این موضوع می‌توانید آن را به اشتراک بگذارید!

کدهای نهایی قسمت سرویس‌

بعد از اینکه کارها را به صورت تمام انجام دادید کدهای‌تان باید به صورت زیر باشد:

import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';

import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import { fromPromise } from 'rxjs/observable/fromPromise';

import { Subject } from 'rxjs/Subject';

import { filter, take, mergeMap } from 'rxjs/operators';

import * as Module from './../../wasm/fibonacci.js';

import '!!file-loader?name=wasm/fibonacci.wasm!../../wasm/fibonacci.wasm';

declare var WebAssembly;

@Injectable()

export class WasmService {

  module: any;

  wasmReady = new BehaviorSubject<boolean>(false);

  constructor() {

    this.instantiateWasm('wasm/fibonacci.wasm');

  }

  private async instantiateWasm(url: string) {

    // fetch the wasm file

    const wasmFile = await fetch(url);

    // convert it into a binary array

    const buffer = await wasmFile.arrayBuffer();

    const binary = new Uint8Array(buffer);

    // create module arguments

    // including the wasm-file

    const moduleArgs = {

      wasmBinary: binary,

      onRuntimeInitialized: () => {

        this.wasmReady.next(true);

      }

    };

    // instantiate the module

    this.module = Module(moduleArgs);

  }

  public fibonacci(input: number): Observable<number> {

    return this.wasmReady.pipe(filter(value => value === true)).pipe(

      mergeMap(() => {

        return fromPromise(

          new Promise<number>((resolve, reject) => {

            setTimeout(() => {

              const result = this.module._fibonacci(input);

              resolve(result);

            });

          })

        );

      }),

      take(1)

    );

  }

}

در پایان

در این آموزش ما شیوه استفاده از قدرت Web Assembly را در اپلیکیشن‌های Angular فرا گرفتیم. امیدوارم این مقاله به شما کمکی برای فراگیری تکنولوژی‌های جدید کرده باشد.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

/@arastoo
ارسطو عباسی
کارشناس تولید و بهینه‌سازی محتوا

کارشناس ارشد تولید و بهینه‌سازی محتوا و تکنیکال رایتینگ - https://arastoo.net

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید