The context
I am currently working on a complex JavaScript application divided into several layers. On one hand, the communication with the server is centralized in a dedicated framework that extensively uses asynchronous executions sequenced by promises. On the other hand, the application is contained inside a SPA and is designed to provide the best user experience.
Promises
To make a long story short, a promise represents the result of an asynchronous operation. It can be:
- pending (by default)
- fulfilled (i.e. succeeded)
- or rejected (i.e. the operation failed)
The most common usage is to wait for the operation completion and chain it with a callback. This is done by calling the method then with one parameter: the function to call on completion.
For instance: // An example of function that uses a promise to signal its completion
function myPromiseBasedFunction() {
var promise = new Promise();
/*
Execute the content of the function and signal the completion using
promise.resolve(). This can be asynchronous (for instance, using an AJAX
call) or synchronous. The caller does not need to know.
*/
return promise;
}
// An example of use:
myPromiseBasedFunction().then(function() {
// Triggered only when the promise is fulfilled (i.e. resolve was called)
});
This offers a convenient way to write asynchronous code that can be chained easily. After creating the promise, the execution starts and even if the result is available immediately (or synchronously), the promise allows you to register the callback function before it signals the completion.
Hence, to work appropriately, a promise must defer its completion execution to let the caller build first the chain of callbacks.
The only reliable way to implement such a code sequence is to use the setTimeout function in order to have the resolve method implementation be called after the registration of the callback function (i.e. calls of method then).
Responsive user interface
As any user interface, the design is polished to provide the best user experience possible. It means that long JavaScript operations are split into chunks and sequenced using setTimeout to prevent any interface freeze and get rid of the annoying long-script running message.
Long running example in Chrome
This long running sampler will allow you to see this dialog. Enter a number of seconds to wait for (usually 100s) and click go!
setTimeout in an inactive tab
Chrome and FireFox share a particularity that I discovered when using several tabs (later, I found that Opera was doing the same but Internet Explorer and Safari are safe). At some point, the application appeared to be 'frozen' when the tab was not active.
For instance, have a look to the following example page. It was designed to print the current time every 100 milliseconds both in the page title and in the page content. If the tab is not active, you will notice that it seems to refresh slower (nearly every second). I also added a real-time monitor that displays red dots if the interval between two calls is greater than 120 ms.
You can find good explanations of the reasons why as well as some possible workarounds by crawling the web:
- Chrome: timeouts/interval suspended in background tabs?
- Chrome and Firefox throttle setTimeout/setInterval in inactive tabs
As it seems that the setTimeout function works fine in a Web Worker thread, I decided to explore this possibility.
Timeout project
I created a new GitHub repository and started to work on a small library that would solve the issue.
Skeleton
This script is based on an immediately-invoked function expression. It has two advantages:
- A private scope for variables and functions
- When invoked, this is translated into the parameter self to provide the global context object
Hooking the APIs
First of all, the JavaScript language is really flexible as it relies on late binding. It means that every time you call a function using a name, the name is first resolved (as any variable) to get the function object.
for instance: /*
In the following example, the JavaScript engine must:
- first resolve the variable "gpf"
- the member "bin" on the previous result
- the member "toHexa" on the previous result
This last result is considered as a function
and called with the parameter 255
*/
var result = gpf.bin.toHexa(255); // result is FF
In a browser, the main context object (the one that contains everything) is the window object. Keep that information in mind, this will be important for the next part.
Hence it is possible to redefine the setTimeout function by assigning a new function to the variable window.setTimeout. I decided to cover the whole timer API, that's why I redefined the followings:
Creating a Web Worker
To create an HTML5 Web Worker you need several things:
- An HTML5 browser (most modern browsers are)
- An URL to load
- A JavaScript code to create it:
var worker = new Worker("source.js");
- A way to communicate with the worker (will be covered in the next part)
Regarding the URL to load, one challenge that I started with is that I wanted the same script to be used not only to redefine the APIs in the main context but also to implement the Web Worker (so that only one file must be distributed). But it is impossible for a script to know how it has been loaded as you don't have its 'current' URL. So I created the method _getTimeoutURL to extract this URL:
- It checks if a script tag with the id "timeout" exists
- Or it checks all script tags for the one finishing with "timeout.js"
- Or it returns "timeout.js"
Regarding the worker creation, the same script is used for the main context as well as the web worker. So I needed a way to distinguish the two situations. This is where the window object can help. Indeed, a worker thread can't access it: the worker object itself is the global context of the thread. That explains why the distinction is made by checking the window typeof.
Main thread / WebWorker communication
The communication between the main thread and the web worker is based on messages: they are asynchronous by nature.
Unless you start messing with the transferList parameter, you can only transmit types that are convertible to a JSON representation.
(This is a highly simplified truth. To be exact, HTML5 introduces the notion of structured clone algorithm used for serializing complex objects.)
To receive messages, you must register on the "message" event using addEventListener
- Either on the created worker object to listen to messages sent from the worker to the main thread
- Or the worker global context to listen to the messages received from the main thread
Other implementation details
To make a long story short, every time you call setTimeout or setInterval, a new record is created in the corresponding dictionary (_timeouts or _intervals) to store the parameters of the call.
Its key is a number that is allocated (incremented) from _timeoutID or _intervalID.
Then a message is sent to the worker thread to execute the timeout function: only the key and the delay are transmitted.
On timeout, the worker sends back a message with the key to the main thread which retrieves the parameters and executes the callback.
Possible improvements
Several aspects of this implementation can be improved:
- Startup time: sometimes, the web worker requires several seconds to run. Because of that, all timeouts may be delayed more than necessary during this phase. An improvement would consist in switching to the new API only when the new thread is ready.
- URL to load: digging on the net, I found a sample where the web worker was initialised using a data: URL. This clearly reduces the dependency with the source script but, then, we need a bootstrap to load the code inside the web worker.
Conclusion
It works and, more important, without modifying the original code! please check the following example page with fix (and don't forget to switch tab).
No comments:
Post a Comment