28 November 2016

Fixing Screen Freeze and Hangs With Web Worker

As web applications become more responsive and advanced today, users may encounter problems from your HTML5 web applications from non-responding script errors to pages that actually hangs forever. Fortunately, thanks to HTML5 web development has given a dose of power that was used to be available to native desktop applications known as the multi-threading.

 Most of my career as a programmer/software developer has revolved around resolving performance issues. I guess there's only a handful in this country who does that. Moving on the topic, multi-threaded programming involves dealing with daemons or background processes or concurrent processes, involves management of buffers, queues asynchronously in order for an application to run smoothly when it's subjected to millions or billions of transactions per second(!).

Introducing the Web Worker, Web Worker is a Javascript API available in all browsers that support HTML5, it works the same way as the Java Thread is mainly used in desktop (SWING, JFC, RCP) and application servers (Apache, JBoss etc). In simple terms, the purpose is to keep the application running smoothly on any given condition. How the Web Worker Works? When an instance of a Web Worker is created, the browser creates a background daemon in order for a web page to run smoothly. One example is when user is querying large chunk of data and at the same scrolling the page, the application must not prevent the user from doing other tasks while the data is loading.

A working sample, while some web developers will dive and start coding on Web Worker API directly, I decided to use an open source web worker queue library from Github https://gist.github.com/kig/1188381 , the main reason for going on this strategy is to simplify handling worker pool or queue that is not readily available in the native HTML Web Worker API.

The Queue Implementation:

WorkCrew = function(filename, count) {
  this.filename = filename;
  this.count = count || 4;
  this.queue = [];
  this.results = [];
  this.pool = [];
  this.working = {};
  this.uuid = 0;
  this.fillPool();
};

WorkCrew.prototype.onfinish = function() {};

WorkCrew.prototype.oncomplete = function(res) {
  return [res.id, res.result];
};

WorkCrew.prototype.addWork = function(work) {
  var id = this.uuid++;
  this.queue.push({id: id, work: work});
  this.processQueue();
  return id;
};

WorkCrew.prototype.processQueue = function() {
  if (this.queue.length == 0 && this.pool.length == this.count) {
    if (this.onfinish)
      this.onfinish();
  } else {
    while (this.queue.length > 0 && this.pool.length > 0) {
      var unit = this.queue.shift();
      var worker = this.pool.shift();
      worker.id = unit.id;
      this.working[worker.id] = worker;
      worker.postMessage(unit.work);
    }
  }
};

WorkCrew.prototype.addWorker = function() {
  var w = new Worker(this.filename);
  var self = this;
  w.onmessage = function(res) {
    var id = this.id;
    delete self.working[this.id];
    this.id = null;
    self.pool.push(this);
    try {
      self.oncomplete({id: id, result: res});
    } catch(e) {
      console.log(e);
    }
    self.processQueue();
  };
  this.pool.push(w);
};

WorkCrew.prototype.fillPool = function() {
  for (var i=0; i    this.addWorker();
  }
};


The Worker Code:

//this where the background process is called that must be in a separate file in this sample we call it //worker.js

self.addEventListener('message', function(e) {
console.log("REMOTE. Received from main script: " + e.data + " -- " + new Date());
self.postMessage(e.data);
}, false);

The Parent client code (this is called from the parent page):

// Create worker pool with 4 workers
 var crew = new WorkCrew("worker.js", 4);

 // Page rendering can only be done here as DOM and parent objects are not accessible
 // inside the Worker thread
 // The result object structure is
 // {
 //   id: work unit ID,
 //   result: message received from worker
 // }
 crew.oncomplete = function(res) {


 // Add some work to the queue.
 // The work unit is postMessaged to one of
 // the workers.

 for (var i = 0;i < someWorkarray.length; i++){
 console.log("Work Crew: Adding work index - " + i + " -- " + new Date());
 var workId = crew.addWork(i);
 }


 // Add an onfinish event handler.
 // Fired when the queue is empty and all workers
 // are free.
 crew.onfinish = function() {
   console.log('Work Crew: All work in queue finished!' + " -- " + new Date());
 };

The code above creates 4 instance of the Web Worker and add multiple work units to the worker it can be hundreds or thousands of tasks but the thing is it will keep your app from freezing, that is if your app really has large amounts of data.