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

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube