04 April 2006

JavaScript, threading, and continuations

I've got a few bits and pieces of my side project working now, and I ran into something unexpected.

The annoying problem

In a simple GUI app, events (like mouse clicks and keystrokes) are all handled in a single, dedicated thread. If event-handling code takes too long to run, the application becomes unresponsive. It works like this everywhere: Java, Windows, XWin—and client-side JavaScript. But as always, JavaScript is... different:

  1. The whole browser doesn't become unresponsive, just your scripts. This is scripts don't run in the browser's main GUI thread. Rather, the browser gives you a separate thread dedicated to scripting events. But don't think this lets you off the hook, because...

  2. The browser detects unresponsive scripts, and believe me, you don't want this to happen. Firefox, for instance, pops up a warning message and offers the user the option to kill your script. If the user says no, the same warning message pops up again a few seconds later.

  3. The usual solution to this problem won't work in a browser. In any other system, you would just take the long-running code and move it into its own thread. But scripts can't spawn threads. Now what?

The annoying solution

I found a workaround using window.setTimeout(). You have to change your code to do its work in chunks and use setTimeout() as a substitute for preemptive thread-switching. It sounds like green threads. Unfortunately, it's much worse than that. Consider this:

    function hog() {
        // code that does three time-consuming things
        spellCheckWikipedia();
        findPrimesInDigitsOfPi();
        buildConcensusOnUsenet();
    }

In a language with green threads, you'd just call sleep(1) (or whatever) between steps.

In JavaScript, you have to write this:

    function hog_start() {
        setTimeout(hog_step1, 10);
    }

    function hog_step1() {
        spellCheckWikipedia();
        setTimeout(hog_step2, 10);
    }

    function hog_step2() {
        findPrimesInDigitsOfPi();
        setTimeout(hog_step3, 10);
    }

    function hog_step3() {
        buildConcensusOnUsenet();
        // phew-- done.
    }

Ugly. The really annoying case is when individual steps are too time-consuming. Suppose buildConcensusOnUsenet() takes an unbounded amount of time to run. Then the function needs to be modified to be "setTimeout()-aware"—a huge pain.

Funny thing is: this is just continuation-passing style. You use setTimeout to "jump to" a continuation. I thought this was cute, like a pun is cute. I've seen continuations simulated via exceptions, but never via event queues.

I guess theoretically I could source-transform JavaScript code written in the green-threads style into JavaScript written in continuation-passing style. Not sure I want to go that far.

No comments: