The Mozilla platform has recently been extended with a new JavaScript library for asynchronous, efficient, file I/O. With this library, developers of Firefox, Firefox OS and add-ons can easily write code that behave nicely with respect to the process and the operating system. Please use it, report bugs and contribute.

Off-main thread file I/O

Almost one year ago, Mozilla started Project Snappy. The objective of Project Snappy is to improve, wherever possible, the responsiveness of Firefox, the Mozilla Platform, and now, Firefox OS, based on performance data collected from volunteer users. Thanks to this real-world performance data, we have been able to identify a number of bottlenecks at all levels of Firefox. As it turns out, one of the main bottlenecks is main thread file I/O, i.e. reading from a file or writing to a file from the thread that also runs most of the code of Firefox and its add-ons.

Let us look at the behavior of a typical main thread file I/O:

The Firefox process initiates some I/O operation (say, reading or writing a few bytes to the disk). Until the operation is complete, the main thread is frozen, which means that the user interface is not updated, that events are not handled, that web pages are not displayed and basically that (almost) nothing happens. The I/O operation is sent to the operating system. The operating system waits until the device is available. The operating system actually performs the I/O operation. The operating system returns control to the Firefox process, which resumes its normal behavior.

Now, operations are typically pretty quick. However, there is a big catch, especially for those among us who like reasoning in terms of algorithmic complexity: item 2 is actually very much non-deterministic. It depends on numerous factors beyond our reach, such as how busy the operating system is at the moment, how busy the drive is at the moment, how busy the other drives are at the moment, how long it has been since the drive was last accessed, whether the device is running on battery power, how much memory is currently available to the operating system, etc.

The end result is that, sometimes, for no apparent reason, a trivial operation such as flushing a buffer, renaming a file or even closing a file, will take 10 seconds. During these ten seconds, the Firefox process is frozen.

For this reason, an important part of Project Snappy is to get rid of main thread file I/O in Firefox and the Mozilla Platform, and replace it with off-main thread file I/O. Let us look at the behavior of typical off-main thread file I/O:

The Firefox process requests an I/O operation from an I/O thread. During this operation, the main thread remains active, the user interface is updated, events are handled, web pages are displayed and basically, everything happens except code that needs the result of the I/O operation to proceed. The I/O thread initiates the I/O operation. The I/O operation is sent to the operating system. The operating system waits until the device is available. The operating system actually performs the I/O operation. The operating system returns control to the Firefox process. The I/O thread triggers the execution of the code that was waiting for the result of the I/O operation.

As we can see, the critical difference between main thread I/O and off-main thread I/O is that the user can keep using the application even while the operating system is busy carrying out the I/O operation. In most languages, the result is typically slightly slower and a little more difficult to write than main-thread operation (if you are curious, you can check out languages such as Rust or Opa, in which this is not the case), but for interactive applications, the benefit far outweighs the cost.

OS.File, chapter 2

The OS.File library has been developed for the specific purpose of letting developers on the Mozilla Platform perform efficient off-main thread file I/O. I have introduced several components of OS.File in past blog entries. Today, let me introduce the asynchronous API for OS.File, as a set of examples:

Copying or renaming a file

Let us copy file “profile.ini” (from the profile directory) to “profile.ini copy” (in the temporary directory). For this purpose, we will use function OS.File.copy.

// Import OS.File Components.utils.import("resource://gre/modules/osfile.jsm"); // Compute the path to some well-known file let source = OS.Path.join(OS.Constants.Path.profileDir, "profile.ini"); let dest = OS.Path.join(OS.Constants.Path.tmpDir, "profile.ini copy"); let promise = OS.File.copy(source, dest); console.log("The copy has started. This message will generally be displayed before it is complete, though"); promise = promise.then(function onSuccess() { console.log("I have successfully copied file", source, "to", dest); });

As you can see, I/O operations return promises, i.e. objects that can be used to trigger some behavior upon completion of the operation. For more details about promises, you may read the documentation of the library – note that this implementation of promises is specific to Firefox but promises are expected to become standard as part of a future version of JavaScript.

Reading/writing the full contents of a file

In the previous example, we used built-in function OS.File.copy. This time, to demonstrate reading and writing, we will read the full contents of the file and write it back:

// Import OS.File Components.utils.import("resource://gre/modules/osfile.jsm"); // Compute the path to some well-known file let source = OS.Path.join(OS.Constants.Path.profileDir, "profile.ini"); // Read the contents of this file let promise = OS.File.read(source); console.log("Currently reading file", source); // During the read operation, the process and JavaScript continue executing // Once the operation is complete, we can display the results let contents; promise = promise.then(function onSuccess(result) { // array is a Uint8Array contents = result; console.log("I have just read", contents.byteLength, "bytes"); }); // Now, let us write this to another file let dest = OS.Path.join(OS.Constants.Path.tmpDir, "profile.ini copy 2"); promise = promise.then(function onSuccess() { return OS.File.writeAtomic(dest, contents, {tmpPath: dest + ".tmp"}); }); // Of course, we can add further instructions whose execution will continue during // the read and write operations console.log("I might be doing some I/O, but I can continue working");

Recall that the read and write operations take place off the main thread, so (unless the file is too large to fit in memory) the complete read and the complete write will actually be quite fast. Also note that the communication of data between threads is quite cheap (buffers are never copied, for one thing).

In this example, we have used function writeAtomic. This function ensures that file “profile.ini copy 2” is not modified unless we are certain that it has been fully written to disk. Using this function considerably reduces the risk of corruption, should the process somehow be stopped during the operation – we have seen this happening because of batteries running out, of crashes, of anti-viruses behaving badly, etc.

Reading and writing text

Reading and writing text is slightly more complex, as text needs to be encoded/decoded. Nothing to worry about, though, now that the StringEncoding API has landed:

let encoder = new TextEncoder(); // Use default encoding (utf-8) let decoder = new TextDecoder(); // Use default encoding (utf-8) // Write to the file let promise = OS.File.writeAtomic(dest, encoder.encode("My text"), {tmpPath: dest + ".tmp"}); promise = promise.then(function onSuccess() { return OS.File.read(dest); }); let text; promise = promise.then(function onSuccess(array) { text = decoder.decode(array); console.log("Here is the text I decoded: ", text); }); // ...

Handling errors

So far, we have been dealing only with successes. However, no I/O library would be complete if it did not let its users deal with runtime errors. Since everything is executed off the main thread, the traditional model of syntactically-scoped exceptions, as featured by JavaScript, cannot be used. Fortunately, promises are also a great mechanism for dealing with asynchronous errors.

let promise = OS.File.read(aFileThatDoesNotExist); promise = promise.then(function onSuccess(contents) { console.log("I have successfully read the contents of the file"); return true; }, function onError(reason) { // reason is an instance of OS.File.Error if (reason.becauseNoSuchFile()) { console.log("Ah well, the file does not seem to exist"); return false; } else { console.log("Some other error", reason); return false; } });

Again, for more details on promise-based error-handling, the best source is the documentation of promises.

Walking directories

A I/O library would hardly be complete if it did not provide facilities for iterating through a directory:

let iterator = new OS.File.DirectoryIterator(OS.Constants.Path.tmpDir); let promise = iterator.forEach(function iter(entry, index) { console.log("I have encountered", entry.name, entry.isDir?"(directory)":"(not a directory)"); }); promise = promise.then(function onSuccess() { iterator.close(); }, function onError(reason) { iterator.close(); throw reason; // Propagate error });

Note that, should you need to, you can close the iterator at any time during the iteration, which will effectively stop the loop. Also, your function iter can return a promise, in which case the operation will be carried out before the loop continues.

And more…

OS.File also offers functions for accessing the details of a file (OS.File.prototype.stat), reading to an already-allocated typed array, getting/setting the current position in the file, getting/setting the current directory, moving/renaming files, deleting files, creating/removing directories, etc. – with more features coming.

Each of these operations is executed off the main thread, to guarantee maximal reactivity. Indeed, only the logistics of synchronization between threads is executed on the main thread.

What now?

Well, now, the library is available as part of Firefox 18. You are invited to test it, try it, break it, file bug reports and requests for features!

If I find time, I will try to blog about the design of OS.File in another blog entry.