Search code examples
javascriptmultithreadingweb-worker

How to get a Webworker really responsive and why setTimeout() not working


I am studying Javascript Web Workers. To do so, I created the page and script below:

worker-test.html

<!DOCTYPE html>
<html>
<head>
    <title>Web Worker Test</title>
    <script src="worker-test.js"></script>
</head>
<body onload="init()">
<form>
    <table width="50%">
        <tr style="text-align: center;">
            <td width="25%">
                <label >Step Time</label>
                <select id="stepTime" onchange="changeStepTime();">
                    <option>500</option>
                    <option>1000</option>
                    <option>2000</option>
                </select>
            </td>
            <td width="25%">
                <button id="btnStart" onclick="return buttonAction('START');">Start</start>
            </td>
            <td width="25%">
                <button id="btnStop" onclick="return buttonAction('STOP');">Stop</start>
            </td>
            <td width="25%">
                <button id="btnClean" onclick="return clean();">Clean</start>
            </td>
        </tr>
        <tr>
            <td colspan="4">
                <textarea id="log" rows="15" style="width: 100%; font-size: xx-large;"  readonly="readonly"></textarea>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

worker-test.js

function id(strId) {
    return document.getElementById(strId);
}

worker = new Worker('worker.js');
worker.onmessage =  function(e) {
    switch(e.data.msg) {
        case "STARTED":
            console.info(e.data);
            id("btnStart").disabled = true;
            id("btnStop").disabled = false;
            break;
        case "STOPPED":
            console.info(e.data);
            id("btnStart").disabled = false;
            id("btnStop").disabled = true;
            break;
        case "TIME":
            id("log").value += e.data.time + '\n';
            break
        default:
            // ...
    }
};

function buttonAction(action) {
    worker.postMessage({msg: action}); 
    return false;
}

function changeStepTime() {
    worker.postMessage({msg: "STEPTIME", stepTime: id("stepTime").value});
}

function clean() {
    id('log').value = ''; 
    return false;
}

function init() {
    id("btnStop").disabled = true;
    changeStepTime();
}

worker.js

var running = false;
var stepTime = 0;

// Replacement of setTimeout().
function pause() {
    var t0 = Date.now();
    while((Date.now() - t0) < stepTime) {
        (function() {}) ();
    }
}

function run() {
    while(running) {
        postMessage({msg: "TIME", time: new Date().toISOString()});
        // pause();
        setTimeout(function() {console.debug("PAUSE!!!")}, stepTime);
    }
}

addEventListener('message', function(e) {
    switch(e.data.msg) {
        case "START":
            running = true;
            console.debug("[WORKER] STARTING. running = " + running);
            run();
            postMessage({msg: "STARTED"});
            break;
        case "STOP":
            running = false;
            console.debug("[WORKER] STOPPING. running = " + running);
            postMessage({msg: "STOPPED"});
            break;
        case "STEPTIME":
            stepTime = parseInt(e.data.stepTime);
            console.debug("[WORKER] STEP TIME = " + stepTime);
            postMessage({msg: "STEP TIME CHANGED"});
            break;
        default:
            console.warn("[WORKER] " + e.data);
    }
});

How the page is supposed to do: When I click in Start Button, it sends a message to worker which call run function. In this function there is a while loop. There, first is sent a message to UI thread with the current time in the to be shown in the <TEXTAREA>. Next, the setTimeout() is called to pause in the time setted by stepTime <SELECT> element. Also, when the loop starts, the Start button must be disabled and the Stop one enabled. When the Stop is clicked, it changes the running flag in the Worker to false and stops the loop. Also, the Start button must be enabled and the Stop one disabled. Always when I click in the buttons, the console shows the message which is sent to worker and next the message that is received by the UI Thread from Worker. When I change the stepTime <SELECT> value, the worker pause time changes. The Clean button simply cleans the <TEXTAREA>.

What I really got: When I click Start, the <TEXTAREA> is filled with current time messages and the controls simply don´t respond. Actually all the browser instances (I tested in Firefox) freeze and after some time crash. Apparently, the setTimeout() is not working in the Worker, althought it works in UI (for example, in console I type setTimeout(function() {console.debug("PAUSE!!!")}, id("stepTime").value);).

A workaround I tried was replace setTimeout() calling by pause() function. I got this: The page (and the browser) is not freezed. The <TEXTAREA> shows the time messages, with the interval initially setted in the <SELECT>. The Clean button works as expected. However, when the loop starts, it shows the console message in worker (console.debug("[WORKER] STARTING. running = " + running);), but not the worker response (postMessage({msg: "STARTED"});). Neither the Start is disabled not the stop is enabled. Besides, if I change the <SELECT> value, it does not change the interval. If I really want change the interval, I need reload the page and set another value.

Looking into the code it is obvious why the response is not being called when I start the loop, because the run() function is a inifinite loop and therefore response command never is reached.

Therefore, I have two questions:

  1. How to change the code to make it really assyncronous and responsive.
  2. Why setTimeout() is not working if it should work according the Web Worker definition.

Thanks,

Rafael Afonso


Solution

  • 1) For the first question: This code:

    function run() {
        while(running) {
            postMessage({msg: "TIME", time: new Date().toISOString()});
            // pause();
            setTimeout(function() {console.debug("PAUSE!!!")}, stepTime);
        }
    }
    

    will hang your browser because it will postMessage as fast as your cpu runs because you are inside in a sort of while(true) which freezes your webworker. Also the pause function will do the same, basically it is a while(true). To make this code run asynchronous, you should control when postMessage is called inside your webworker, for example every 10% of your job completion ... not inside a while true.

    2) For the second question: this line of code inside the while

    setTimeout(function() {console.debug("PAUSE!!!")}, stepTime);
    

    will not pause it, it does not affect the loop. In javascript there is not a sleep method like the php one.

    To have the effect you need replace the run function with this one:

    function run() {
        if(running){
            postMessage({msg: "TIME", time: new Date().toISOString()});
            setTimeout(function() { run(); }, stepTime);
        }
    }