Summary

One of the key components of OS.File is the Schedule API, a tiny yet powerful JavaScript core designed to considerably simplify the development of asynchronous modules. In this post, we introduce the Schedule API.



Introduction

In a previous post, I introduced OS.File , a Mozilla Platform library designed make the life of developers easier and to help them produce high-performance, high-responsiveness file management routines.

In this post, I would like to concentrate on one of the core items of OS.File : the Schedule API. Note that the Schedule API is not limited to OS.File and is designed to be useful for all sorts of other modules.

Overview

One of the most important aspect of any user-facing application is its responsiveness. Contrary to intuition, however, responsiveness is much less a matter of speed than one of tempo: not necessarily attempting to perform tasks as fast as possible, but rather attempting to perform them in a manner that does not disrupt the user experience. In the Mozilla Platform, disrupting the user experience is synonymous with freezing the user interface.

Avoiding this is the art of asynchronous, non-blocking operations, and this is the reason we built the Schedule API.

The Schedule API is a tiny JavaScript module designed to simplify considerably the development of asynchronous code. It is based on Joseph Walker’s excellent Promises API [1] and it lies at the core of OS.File’s asynchronous operations.

Before detailing the full API, let us start with a few simple examples. Consider a common situation: some treatment needs to be done soon, but not immediately, as this would otherwise disrupt the user experience. For this purpose, JavaScript offers function setTimeout, which schedules the execution of the treatment to later:

function doItLater() { window.setTimeout(function() { var result = fibonacci(10); });//Schedule the execution for later }//Wait, how do we get the result?

However, setTimeout offers no help for informing the rest of the code that the execution is complete. While there are many solutions to this problem – I am sure that dozens can be found on StackOverflow – perhaps the most developer-friendly is that of promises:

function doItLater() { var promise = new Promise(); window.setTimeout(function() { var result = fibonacci(10); promise.resolve(result); });//Schedule the execution for later return promise;//And return immediately a promise. }

With this code, we have introduced the promise of a future result. Once we have obtained a promise, we can simply add a reaction that will be triggered once the promise is resolved:

doItLater().then(function(result) { alert("I have just finished computing Fibonacci: "+result); })

Or we can chain promises to obtain other promises:

var promise2 = doItLater().chainPromise(function(result) { return sieve(result); })

Note that these two extracts are compatible with each other. If we write both, we will be informed once the first treatment is complete and a second treatment will also take place immediately, one whose result is promised as promise2 .

Unfortunately, a few problems appear with our code. Firstly, window.setTimeout is unfortunately not available in all contexts. While workarounds are available for non-UI modules and background threads, writing any reusable asynchronous is a somewhat painful that can quickly lead to unreadable code. Chaining such treatments becomes even worse.

The second issue is that our call fibonacci(10) might well be quite long itself and might very well disrupt the user experience if we do nothing about it.

Let us start by addressing the first issue, with the Schedule module:

Components.utils.import("resource://gre/modules/schedule.jsm");

Function Schedule.soon replace most uses of window.setTimeout :

function doItLater() { return Schedule.soon(function() { return fibonacci(10); }); }

Or, more concisely:

function doItLater() { return Schedule.soon(fibonacci, 10); }

Or, to delay the execution of the task by 100ms:

function doItLater() { return Schedule.soon({delay: 100}, fibonacci, 10); }

The Schedule API also extends Promises with a new method soon :

var promise2 = doItLater().soon(function(result) { return sieve(result); })

Just like chainPromises , soon adds new treatments that are triggered only once the promise is resolved. However, and by opposition to chainPromises , these treatments are executed later, without disrupting the user experience.

With the combination of Schedule and Promises, it is easy to see how we could write a version of the Fibonacci function that is computed progressively:

//Compute the fibonacci function of n, as a background task function fibo(n) { if (n == 1 | n == 0) { return Schedule.alreadyComplete(1); //Promise a result and deliver it immediately } else { var tasks = [];//Launch two tasks tasks.push(fibo(n - 1)); tasks.push(fibo(n - 2)); return Promise.group(tasks).soon(function(results) { return results[0] + results[1]; }) } }

Nice, readable and non-disruptive. Plus, for debugging purposes, or if you wish to add some visual feedback, you can attach additional observers to promises.

The API

So far, we have made use of the following functions of the Promises API:

Promise ;

; Promise.resolve ;

; Promise.prototype.chainPromise ;

; Promise.prototype.then ;

; Promise.group ;

I am trying to convince Joseph to blog about this very nice API, so you should be able to learn more about it on his blog, one of these days.

We have also made use of the following functions of module Schedule:

Schedule.soon ;

; Promise.prototype.soon ;

; Schedule.alreadyComplete .

While these methods can already be very useful to help you write or rewrite asynchronous code, the Schedule API goes a little bit further and introduces Scheduling Categories and Rendez-vous points.

Categories

While interleaving tasks is a good way to ensure that all tasks progress, some tasks need a little tighter control. Typically, the result of successive operations on a file depend on their order of execution – closing a file before reading from it is generally not a very good idea. Similarly, for performance reasons, an application should generally avoid reading from several files simultaneously, as this can slow down not only the application but the whole system.

In the Schedule API, this is materialized as Scheduling Categories. When several tasks are placed in the same category, any call to Schedule.soon or Promise.prototype.soon guarantees that these tasks will be executed in the order in which they were added and that only one of these task is executed at a time. This does not prevent tasks from other categories from being interleaved.

If, for instance, we wish to serialize the computation of Fibonacci’s function, we may declare a category as follows:

var Fibo = new Schedule.Category("Fibonacci's function");

and replace the final function call of the previous listing with

soon({category: Fibo}, function(results) { return results[0] + results[1]; })

Thus categorized, Fibonacci’s functions will take less resources to compute, and will be even nicer to the rest of the application and the system, though the total computation will probably last a little longer.

Rendez-vous

One of the original reasons for which we started developing the Schedule API was to simplify asynchronous initialization on the Mozilla Platform and ensure that whichever parts of initialization were not strictly required at start-up could be simply and safely postponed.

For this purpose, the Schedule API provides one additional function: Schedule.on . This function lets developers register code to be executed only once some condition is met, as follows:

Schedule.on("startup-complete").soon(delayedInitializer);

With this line, function delayedInitializer will be executed only once “startup-complete” is resolved. Note that, by opposition to DOM-style events, if “startup-complete” has already been resolved, delayedInitializer will be immediately scheduled for execution. This simple change solves a number of refactoring headaches.

By the way, Schedule.on("startup-complete") is just a promise. To resolve it, simply call resolve :

Schedule.on("startup-complete").resolve();

And that’s it. Welcome to the Schedule API!

Status

Everything that was presented in this post is implemented, unit-tested – although not battle-tested – used in OS.File , and currently under review. There is no ETA yet, due to dependencies, but I hope that we will be able to push it soon.

There is one important feature that I wish to add at some point: multi-threading. While there are limitations to what we can transmit between threads, there is no reason for which we could not schedule code for execution not on the main thread, but on a pool of workers. There will be limitations, but I believe that I have a pretty good idea of how to implement this, and if I succeed, this can expand considerably our horizons. I have small chunks of code written, but no ETA for a prototype.

Wrapping this up

The Schedule API is a simple module (only 5 public functions) designed to make our life easier when writing asynchronous code or refactoring code for asynchronicity.

I hope that you will find it useful.

[1] Thanks to Panos for point this code to me. I had previously reimplemented my own brand of Promises, but having two of them in the Mozilla Platform codebase was simply pointless.

edit Clarified the fact that calling fibonacci(10) in a setTimeout will not magically make everything non-disruptive. Thanks for the feedback, Axel.