The Practical Client

Tools for Debugging Web Workers

Web workers in TypeScript give you concurrent processing but they can be awkward to debug. However, if you set up your web worker code as just another function, you can simplify debugging (or even build your web worker dynamically at run time).

In a previous column, I showed how to start a web worker to handle concurrent processing in client-side code. As I discussed, starting a web worker effectively gives you a parallel thread to the one used by your UI -- a thread to which you can offload processing. You can trigger that processing through the Worker object's postMessage method.

However, debugging a web worker can be challenging. I've got a strategy for simplifying that debugging process but, before I can describe it, I need to discuss how you can dynamically build a web worker at runtime or just embed it in the rest of your code.

Dynamic Web Workers
While you can put your web worker code in a separate file from the code that calls it, that's not essential. You can create a worker by passing a string of code and a type of "application/javascript" to the constructor of a Blob object. Once the Blob object is created, you can generate a URL from the Blob using the URL object's createObjectURL method. This allows you to create a web worker from a string, as in this example which creates a web worker that echoes back whatever is sent to it:

let b: Blob
b = new Blob(["onmessage = function(e) { postMessage(e.data); }"], { type: 'application/javascript' });
let url: string;
url = window.URL.createObjectURL(b);
w = new Worker(url);
Passing a string to a Blob object does allow you to dynamically assemble your worker code at run time. However, it's not without problems. First, of course, the code in your string will have to be in JavaScript rather than TypeScript because the string passed to the Blob constructor at runtime won't be compiled. Second, you'll get no compile-time checking because the code is in a string. And, finally, that string is no easier to debug (and considerably harder to read) than code in a separate file.

Embedded Web Workers
If your concurrent code is a standalone function then, for debugging purposes, it might be easier to keep your concurrent code in a regular function, execute it synchronously, and switch to running it concurrently only when you've got your code working. In this strategy, you pass the Blob object a function in your file into converted to a string using the function's toString method. At compile time, the function name will refer to a TypeScript function but at runtime that same name will refer to the compiled JavaScript version of the function.

To have the function start running as soon as the Worker object is instantiated, though, you'll need to wrap it in parentheses and add the double parentheses that invoke the function. This concurrentFn function is self-starting, for example:

(function concurrentFn() { ...function code... })();

Putting all of that together, this code passes the string version of a function named concurrentFn to a Blob constructor (with the necessary parentheses) and then runs it as a worker:

let fn: string;
fn = "(" + concurrentFn.toString() + ")()";
b = new Blob([fn], { type: 'application/javascript' })
url = window.URL.createObjectURL(b);
w = new Worker(url);

function concurrentFn(): void { ...function code... }

This is, though, a far from a perfect solution because you've given up the Worker object's postMessage method. To actually test a reasonable facsimile of your function you need an alternative mechanism to pass parameters to your function. You'll also need to ensure that you can cleanly switch from this mechanism back to postMessage as you move from synchronous to concurrent code.

Structuring Your Worker Function
My solution has been to put the code that I want to execute concurrently in a nested function inside the function I want to run concurrently. I then call my nested function from an onmessage handler, also in this "concurrent function." To provide access to my nested function, I have my concurrent function return the nested function to the calling code. As a result, my concurrent function looks like the one below (in practice, I'd replace the any types in this code with the actual types accepted and returned by processData):

Here's a concurrent function to support debugging:

function concurrentFn(): (e:MessageEvent) => any 
{
   this.onmessage = function (e: MessageEvent): void {
      (<any>postMessage)( this.processData(e.data) );
   }

   this.processData = (d: any): any => {
      ...concurrent code to process d..;
      return ...result of processing...;
   }
   return this.processData;
}

When I want to execute the code concurrently as a web worker, I instantiate it through a Blob. When I want to debug synchronously, I call concurrentFn to catch the processData function that's returned. From there on, in debug mode, instead of calling onmessage, I call the processData function. That does lead to an issue around the format of the data passed to and from my method.

I use a boolean variable called Concurrent to distinguish between when I want concurrency or synchronous processing. To simplify switching between calling my concurrentFn function and the Worker object, I first define the signature for my concurrentFn object as type. I then declare a union variable that can hold either a Worker object or my concurrentFn function. That code looks like this (again, I'd change the any type to the types actually passed to and returned by my method):

const Concurrent: boolean = false; 
type workerFn = (any) => any;
let w: (Worker | workerFn);

Typical code to either instantiate a Worker object or retrieve my processData method looks like Listing 1.

Listing 1: Controlling Whether Code Runs Concurrently or Synchronously

if (Concurrent) {
  let fn: string;
  let b: Blob

  fn = "(" + longRunning.toString() + ")()";
  b = new Blob([fn], { type: 'application/javascript' })
  let url: string;
  url = window.URL.createObjectURL(b);

  w = new Worker(url);
  w.onmessage = (e: MessageEvent) => {processReturnedData(e);};
}
else {
  let w = longRunning();
}

Later in my code, when I want to post a message to my concurrent code, I have code like this:

if (Concurrent) {
  w.postMessage("A123");
}
else {
  processReturnedData( w("A123") );
}

This is a lot of extra code and, eventually, I created a WorkerFake class that accepts two parameters: the function to be run and a boolean that indicates if the function should be run concurrently. When run concurrently, it wraps the function in code that allows the function to be triggered by calling postMessage from the main code. It's a generic method so you must specify the type of the data you'll be passing to the function and will be getting back from it (you can see it in Listing 2). Typical code to use this would look like this:

let wf: WorkerFake<string, Customer>;
wf = new WorkerFake<string, Customer>(false, getCustomer);
wf.onmessage = (e: MessageEvent) => { ...code to process Customer object... };
wf.postMessage("A123");

function getMockCustomer(id: string): Customer {…code to retrieve Customer…}

Of course, if my concurrent functions ran correctly the first time, I wouldn't bother with any of this. It could happen.

Listing 2: A Class for Running Functions Synchronously or Concurrently
class WorkerFake<T,TValue> {
  w: Worker;
  fnWorker: (T) => TValue;
  fnOut: (MessageEvent) => TValue;

  constructor(public concurrent: boolean, fn: (any) => any) {
    if (concurrent) {
      let fns: string;
      fns = "onmessage = function (e) { postMessage(" + fn.name + "(e.data)); }; "  + fn.toString(); 
      let b: Blob;
      b = new Blob([ fns ], { type: 'application/javascript' })
      this.w = new Worker( window.URL.createObjectURL(b) );
    }
    else {
      this.fnWorker = fn;
    }
  }

  postMessage(parm: any) {
    if (this.concurrent) {
      this.w.postMessage(parm);
    }
    else {
      let me: MessageEvent;
      me = new MessageEvent("message", { data: this.fnWorker(parm) });
      this.fnOut(me);
    }
   }

   set onmessage(fno: (MessageEvent) => any) {
     if (this.concurrent) {
       this.w.onmessage = fno;
     }
     else {
       this.fnOut = fno;
     }
   }         
}

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube