Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript : setTimeout and interface freezing

Context

I've got about 10 complex graphs which take 5sec each to refresh. If I do a loop on these 10 graphs, it takes about 50 seconds to refresh. During these 50 seconds, the user can move a scrollbar. If the scrollbar is moved, the refresh must stop and when the scrollbar stops to move, the refresh occurs again.

I'm using the setTimeout function inside the loop to let the interface refresh. the algorithm is :

  • render the first graph
  • setTimeout(render the second graph, 200)
  • when the second graph is rendered, render the third one in 200ms, and so on

The setTimeout allows us to catch the scrollbar event and to clearTimeout the next refresh to avoid to wait 50sec before moving the scrollbar...

The problem is that it does not run anytime.

Take the simple following code (you can try it in this fiddle : http://jsfiddle.net/BwNca/5/) :

HTML :

<div id="test" style="width: 300px;height:300px; background-color: red;">

</div>
<input type="text" id="value" />
<input type="text" id="value2" />

Javascript :

var i = 0;
var j = 0;
var timeout;
var clicked = false;

// simulate the scrollbar update : each time mouse move is equivalent to a scrollbar move
document.getElementById("test").onmousemove = function() {

    // ignore first move (because onclick send a mousemove event)
    if (clicked) {
        clicked = false;
        return;
    }

    document.getElementById("value").value = i++; 
    clearTimeout(timeout);
}

// a click simulates the drawing of the graphs
document.getElementById("test").onclick = function() {
    // ignore multiple click
    if (clicked) return;

    complexAlgorithm(1000);    
    clicked = true;   
}

// simulate a complexe algorithm which takes some time to execute (the graph drawing)
function complexAlgorithm(milliseconds) {
  var start = new Date().getTime();
  for (var i = 0; i < 1e7; i++) {
    if ((new Date().getTime() - start) > milliseconds){
      break;
    }
  }   

  document.getElementById("value2").value = j++;

  // launch the next graph drawing
  timeout =  setTimeout(function() {complexAlgorithm(1000);}, 1);
}

The code does :

  • when you move your mouse into the red div, it updates a counter
  • when you click on the red div, it simulates a big processing of 1sec (so it freezes the interface due to javascript mono thread)
  • after the freezing, wait 1ms, and resimulate the processing and so on until the mouse move again
  • when the mouse move again, it breaks the timeout to avoid infinite loop.

The problem

When you click one time and move the mouse during the freeze, I was thinking that the next code that will be executed when a setTimeout will occurs is the code of the mousemove event (and so it will cancel the timeout and the freeze) BUT sometimes the counter of click gains 2 or more points instead of gaining only 1 point due to the mouvemove event...

Conclusion of this test : the setTimeout function does not always release resource to execute a code during a mousemove event but sometimes kept the thread and execute the code inside the settimeout callback before executing another code.

The impact of this is that in our real example, the user can wait 10 sec (2 graphs are rendered) instead of waiting 5 seconds before using the scrollbar. This is very annoying and we need to avoid this and to be sure that only one graph is rendered (and other canceled) when the scrollbar is moved during a render phase.

How to be sure to break the timeout when the mouse move ?

PS: in the simple example below, if you update the timeout with 200ms, all runs perfectly but it is not an acceptable solution (the real problem is more complex and the problem occurs with a 200ms timer and a complex interface). Please do not provide a solution as "optimize the render of the graphs", this is not the problem here.

EDIT : cernunnos has a better explanation of the problem : Also, by "blocking" the process on your loop you are ensuring no event can be handled until that loop has finished, so any event will only be handled (and the timeout cleared) inbetween the execution of each loop (hence why you sometimes have to wait for 2 or more full executions before interrupting).

The problem is exactly contains in bold words : I want to be sure to interrupt the execution when I want and not to wait 2 or more full executions before interrupting


Second EDIT :

In summary : takes this jsfiddle : http://jsfiddle.net/BwNca/5/ (the code above).

Update this jsfiddle and provide a solution to :

Mouse move on the red div. Then click and continue moving : the right counter must raise only once. But sometimes it raises 2 or 3 times before the first counter can run again... this is the problem, it must raise only once !

like image 820
Jerome Cance Avatar asked Apr 12 '13 13:04

Jerome Cance


People also ask

Is setTimeout blocking JavaScript?

Explanation: setTimeout() is non-blocking which means it will run when the statements outside of it have executed and then after one second it will execute. All other statements that are not part of setTimeout() are blocking which means no other statement will execute before the current statement finishes.

Do I need to clean up setTimeout?

clearTimeout is only necessary for cancelling a timeout. After the timeout fires, it can safely be left alone. clearInterval is much more typically necessary to prevent it from continuing indefinitely.

Does setTimeout affect performance?

No significant effect at all, setTimeout runs in an event loop, it doesn't block or harm execution.

Does setTimeout pause execution?

No, setTimeout does not pause execution of other code.


1 Answers

The BIG problem here is setTimeout is unpredictable once it started, and especially when it is doing some heavy lifiting.

You can see the demo here: http://jsfiddle.net/wao20/C9WBg/

var secTmr = setTimeout(function(){
    $('#display').append('Timeout Cleared > ');
    clearTimeout(secTmr);        

    // this will always shown
    $('#display').append('I\'m still here! ');
}, 100);

There are two things you can do to minimize the impact on the browser performance.

Store all the intances of the setTimeoutID, and loop through it when you want to stop

var timers = []

// When start the worker thread
timers.push( setTimeout(function () { sleep(1000);}, 1) );

// When you try to clear 
while (timers.length > 0) {
     clearTimeout(timers.pop());
}

Set a flag when you try to stop process and check that flag inside your worker thread just in case clearTimeout failed to stop the timer

// Your flag 
var STOPForTheLoveOfGod = false;

// When you try to stop 
STOPForTheLoveOfGod = true; 
while (timers.length > 0) {       
     clearTimeout(timers.pop()); 
}

// Inside the for loop in the sleep function 
function sleep(milliseconds) {
    var start = new Date().getTime();
    for (var i = 0; i < 1e7; i++) {
        if (STOPForTheLoveOfGod) {
            break;
        }       

        // ...  
    }
}

You can try out this new script. http://jsfiddle.net/wao20/7PPpS/4/

like image 147
Du D. Avatar answered Nov 15 '22 19:11

Du D.