pseudoThreading Your Way to Happiness with Generators

Introduction

A common piece of functionality that extension developers wish to do is to create a thread to do work in without blocking the application.  For pure-Javascript extensions, there are a few mechanisms to achieve this with.  nsIThreadManager is one mechanism.  Unfortunately, the syntax is a bit arcane, and it's burdened by some fairly strict restrictions, such as not being able to touch any UI (including the window, DOM, etc.).

Fortunately, Javascript1.7 introduced the concept of Iterators & Generators.  We can capitalise on generators to create "pseudoThreads" in a much simpler, less restrictive, way than using nsIThreads.  Songbird utilises this concept of pseudoThreads in a few places, such as the web playlist scraper, and in the Concerts & ♪Photo extensions.

Concepts

Essentially, generators allow you define an iterative piece of code that remembers its state across invocations.  The state is returned/given-up when a yield statement is encountered.  What this means is that when you have a function like:
function myGenerator() {
   while (var i=0; i<100; i++) {
      dump("hello world: " + i + "\n");
      yield i;
   }
}
Your first call of myGenerator() will result in "hello world: 0" being dumped to the console, and myGenerator returning a value of 0.  Your second call to myGenerator() will result in "hello world: 1" and a return value of 1.  Your third call will result in "hello world: 2" and a return value of 2, and so on so forth.

For a more exhaustive and thorough explanation of generators & iterators, please read the MDC article.

CPU-intensive Operations

One problem that is fairly common is that developers will try to do something that can be pretty CPU intensive.  For Concerts & the ♪Photo extensions, they try to enumerate every media item in the library and get/set properties.  When these operations run, the respective loops and enumerators they use consume the application's attention, and won't relinquish it to allow it to process other events in Songbird... such as events for redrawing or painting the application window, thus giving the user the appearance of Songbird having frozen or locked up.

For instance, to use a real world example, ♪Photo originally had the following code:

function triggerScan(list) {
	var artists = list.getDistinctValuesForProperty(SBProperties.artistName);
	while (artists.hasMore()) {
		var artistName = artists.getNext();
            // do some long complicated bit of work
	}
	
	var albums = list.getDistinctValuesForProperty(SBProperties.albumName);
	while (albums.hasMore()) {
		var albumName = albums.getNext();
		// do even more longer crazier complicated bit of work
	}
}

Don't worry too much about what this code does (since obviously the example is incomplete), but basically it would, given a playlist, enumerate all the artists in that playlist and fetch artist artwork for each artist.  Once it had that, it would do the same, except for album artwork.  Suffice to say, these were rather length and time intensive calls that, for a large library, would give the appearance of Songbird having frozen.

Threading with pseudoThread

Converting your code to a pseudoThread driven generator is composed of 3 simple steps... one of which is copy & pasting code.  How easy is that?

pseudoThread()

Fortunately, pvh put together a quick shortcut called pseudoThread.  In a nutshell, pseudoThread is the following:

function pseudoThread(gen) {
  var thisGen = this;    
  var callback = {
    observe: function(subject, topic, data) {
      // we are only interested in timer callbacks, not other messages.
      if (!topic == "timer-callback") return;
      try {
        gen.next();
      } catch (e) {
        threadTimer.cancel();
        threadTimer = null; // break XPCOM cycle
        gen.close();
        // a StopIteration message exception indicates a normal generator shutdown
        if (!(e instanceof StopIteration)) { 
          Components.utils.reportError(e);
        }
      };
    }
  }
  var threadTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
  threadTimer.init(callback, 0, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
}

If you include the above Javascript code in your add-on, you'll have a super-quick way to start pseudoThreading your JS code.  What pseudoThread does is wrap a given generator function (more on this in a bit) with a nsITimer (a repeating timer that fires repeatedly, in the above case: every "0" ms).  Remember that with generators, state is remembered across invocations... so this effectively causes your generator to be called repeatedly every time it hits a yield statement until your generator function either throws an exception or returns without a yield.

Turning your function into a generator

The second step is converting your function into a generator.  Here are the steps required for doing that:

  • Insert yield statements anywhere you want the application to "breathe".
That's pretty much it.  For instance, to convert the above triggerScan() method, we simply insert two yield statements:
function triggerScan(list) {
	var artists = list.getDistinctValuesForProperty(SBProperties.artistName);
	while (artists.hasMore()) {
		var artistName = artists.getNext();
            // do some long complicated bit of work
	}
	
	yield;
	var albums = list.getDistinctValuesForProperty(SBProperties.albumName);
	while (albums.hasMore()) {
		var albumName = albums.getNext();
		// do even more longer crazier complicated bit of work
		yield;
	}
}

Any time you yield, you give the rest of the application an opportunity to process all the messages that have queued up. You should yield regularly during any potentially long processes. Here, we're assuming that the artists loop will never be so slow that the user sees the app as "hung" while it is running, but that the slower albums loop will take longer. Where should you put yours? You can either use the Venkman JS Profiler to help find slow places, or you can just experiment and find the places that go slowly. Be sure to test the worst, slowest possible situation you can imagine! Remember: your users are out to get you.

Invoking The Thread

The very last step needed is invoking your thread. Change your calls to wrap them with "pseudoThread()" like this:
pseudoThread(triggerScan(list));

Summary

That's it.  So to recap:
  1. Copy/paste the pseudoThread() definition above
  2. Insert a few yield statements into your code
  3. Change the way you call your code to wrap it in a pseudoThread instance
If you have any questions, feel free to drop by #songbird on IRC, or post a question to our songbird-dev email list and we'll be happy to help.

Caveat Emptor

You get what you pay for. This isn't a real thread, and it will make things run more slowly. Most confusingly, it doesn't work with nested functions. Let me illustrate. This will not work.
function a() {
  while(true) {
    b();
  }
}
function b() {
  for (var i = 0; i < 100; i++) 
   yield i;
}
pseudoThread(a());
The "a()" function does not contain a yield keyword, so it cannot be driven by the pseudoThread. Similarly, the b() function is not being called with generator-style syntax, so it is being restarted each time. If this doesn't make sense to you, think about it more, and read the Iterators and Generators documentation. If it still doesn't make sense, come into #songbird.
Tag page
You must login to post a comment.