Nick Bolles
Nick Bolles

Reputation: 439

Angular Service and Web Workers

I have an Angular 1 app that I am trying to increase the performance of a particular service that makes a lot of calculations (and probably is not optimized but that's besides the point for now, running it in another thread is the goal right now to increase animation performance)

The App

The app runs calculations on your GPA, Terms, Courses Assignments etc. The service name is calc. Inside Calc there are user, term, course and assign namespaces. Each namespace is an object in the following form

{
    //Times for the calculations (for development only)
    times:{
        //an array of calculation times for logging and average calculation
        array: []

        //Print out the min, max average and total calculation times
        report: function(){...}
    },

    //Hashes the object (with service.hash()) and checks to see if we have cached calculations for the item, if not calls runAllCalculations()
    refresh: function(item){...},

    //Runs calculations, saves it in the cache (service.calculations array) and returns the calculation object
    runAllCalculations: function(item){...}
}

Here is a screenshot from the very nice structure tab of IntelliJ to help visualization

Service Structure

What Needs To Be Done?

  1. Detect Web Worker Compatibility (MDN)
  2. Build the service depending on Web Worker compatibility

    a. Structure it the exact same as it is now

    b. Replace with a Web Worker "proxy" (Correct terminology?) service

The Problem

The problem is how to create the Web Worker "Proxy" to maintain the same service behavior from the rest of the code.

Requirements/Wants

A few things that I would like:

The Question

...I guess there hasn't really been a question thus far, it's just been an explanation of the problem...So without further ado...

What is the best way to use an Angular service factory to detect Web Worker compatibility, conditionally implement the service as a Web Worker, while keeping the same service behavior, keeping DRY code and maintaining support for non Web Worker compatible browsers?

Other Notes

I have also looked at VKThread, which may be able to help with my situation, but I am unsure how to implement it the best.

Some more resources:

Upvotes: 3

Views: 3370

Answers (2)

vadimk
vadimk

Reputation: 1471

I am the author of the vkThread plugin which was mentioned in the question. And yes, I developed Angular version of vkThread plugin which allows you to execute a function in a separate thread.

Function can be defined directly in the main thread or called from an external javascript file.

Function can be:

  • Regular functions
  • Object's methods
  • Functions with dependencies
  • Functions with context
  • Anonymous functions

Basic usage:

/* function to execute in a thread */
function foo(n, m){ 
    return n + m;
}

// to execute this function in a thread: //

/* create an object, which you pass to vkThread as an argument*/
var param = {
      fn: foo      // <-- function to execute
      args: [1, 2] // <-- arguments for this function
    };

/* run thread */
vkThread.exec(param).then(
   function (data) {
       console.log(data);  // <-- thread returns 3 
    }
);

Examples and API doc: http://www.eslinstructor.net/ng-vkthread/demo/

Hope this helps,

--Vadim

Upvotes: 0

Tom&#225;š Zato
Tom&#225;š Zato

Reputation: 53119

In general, good way to make a manageable code that works in worker - and especially one that also can run in the same window (eg. when worker is not supported) is to make the code event-driven and then use simple proxy to drive the events through the communication channel - in this case worker.

I first created abstract "class" that didn't really define a way of sending events to the other side.

   function EventProxy() {
     // Object that will receive events that come from the other side
     this.eventSink = null;
     // This is just a trick I learned to simulate real OOP for methods that
     // are used as callbacks
     // It also gives you refference to remove callback
     this.eventFromObject = this.eventFromObject.bind(this);
   }
   // Object get this as all events callback
   // typically, you will extract event parameters from "arguments" variable 
   EventProxy.prototype.eventFromObject = (name)=>{
     // This is not implemented. We should have WorkerProxy inherited class.
     throw new Error("This is abstract method. Object dispatched an event "+
                     "but this class doesn't do anything with events.";
   }

   EventProxy.prototype.setObject = (object)=> {
     // If object is already set, remove event listener from old object
     if(this.eventSink!=null)
       //do it depending on your framework
       ... something ...
     this.eventSink = object;
     // Listen on all events. Obviously, your event framework must support this
     object.addListener("*", this.eventFromObject);
   }
   // Child classes will call this when they receive 
   // events from other side (eg. worker)
   EventProxy.prototype.eventReceived = (name, args)=> {
     // put event name as first parameter
     args.unshift(name);
     // Run the event on the object
     this.eventSink.dispatchEvent.apply(this.eventSink, args);
   }

Then you implement this for worker for example:

   function WorkerProxy(worker) {
     // call superconstructor
     EventProxy.call(this);
     // worker
     this.worker = worker;
     worker.addEventListener("message", this.eventFromWorker = this.eventFromWorker.bind(this));
   }

   WorkerProxy.prototype = Object.create(EventProxy.prototype);
   // Object get this as all events callback
   // typically, you will extract event parameters from "arguments" variable 
   EventProxy.prototype.eventFromObject = (name)=>{
       // include event args but skip the first one, the name
       var args = [];
       args.push.apply(args, arguments);
       args.splice(0, 1);
       // Send the event to the script in worker
       // You could use additional parameter to use different proxies for different objects
       this.worker.postMessage({type: "proxyEvent", name:name, arguments:args});
   }
   EventProxy.prototype.eventFromWorker = (event)=>{
       if(event.data.type=="proxyEvent") {
           // Use superclass method to handle the event
           this.eventReceived(event.data.name, event.data.arguments);
       }
   }

The usage then would be that you have some service and some interface and in the page code you do:

// Or other proxy type, eg socket.IO, same window, shared worker...
var proxy = new WorkerProxy(new Worker("runServiceInWorker.js"));
//eg user clicks something to start calculation
var interface = new ProgramInterface(); 
// join them 
proxy.setObject(interface);

And in the runServiceInWorker.js you do almost the same:

importScripts("myservice.js", "eventproxy.js");
// Here we're of course really lucky that web worker API is symethric
var proxy = new WorkerProxy(self);
// 1. make a service
// 2. assign to proxy
proxy.setObject(new MyService());
// 3. profit ...

In my experience, eventually sometimes I had to detect on which side am I but that was with web sockets, which are not symmetric (there's server and many clients). You could run into similar problems with shared worker.

You mentioned Promises - I think the approach with promises would be similar, though maybe more complicated as you would need to store the callbacks somewhere and index them by ID of the request. But surely doable, and if you're invoking worker functions from different sources, maybe better.

Upvotes: 2

Related Questions