This question already has answers here:
How to avoid freezing the browser when doing long-running computations in Javascript
(5 answers)
Closed 7 years ago.
Let's say I have a long-running JavaScript process that can be started by clicking a
button. Once it runs, it freezes the browser. The process consists of a long loop
that repeats some workload where one iteration takes comparatively little time. Here is
a stripped down version of such a 'loop-like' process:
<html>
<head>
<script>
var Process = function(start) {
this.start = start;
}
Process.prototype.run = function(stop) {
// Long-running loop
for (var i = this.start; i < stop; i++) {
// Inside the loop there is some workload which
// is the code that is to be debugged
console.log(i);
}
}
var p = new Process(100);
window.onload = function() {
document.getElementById("start").onclick = function() {
p.run(1000000000);
}
}
</script>
</head>
<body>
<input id="start" type="button" value="Start" />
</body>
</html>
Due to the browser freeze, debugging a loop-like script is not easy. One alternative to avoid browser freeze is using a web worker. The drawback of that approach is the poor debuggabilty of web workers: Tools like Firebug are not supported.
Is there a way to avoid browser freeze while maintaining debuggabilty? If yes, the script could be debugged until it is stable and delegated to a web worker for production.
It turns out there is a way to achieve this. Using a Queue data structure
(e.g. http://code.stephenmorley.org/javascript/queues/), an interval timer and some small
modification to the control flow of the original process one can build a GUI that doesn't
freeze the browser, leaves the process fully debuggable and even allows additional features
like stepping, pausing and stopping.
Here is how it goes:
<html>
<head>
<script src="http://code.stephenmorley.org/javascript/queues/Queue.js"></script>
<script>
// The GUI controlling process execution
var Gui = function(start) {
this.timer = null; // timer to check for inputs and/or commands for the process
this.carryOn = false; // used to start/pause/stop process execution
this.cmdQueue = new Queue(); // data structure that holds the commands
this.p = null; // process instance
this.start = start;
this.i = start; // input to the modified process
}
Gui.prototype = {
/**
* Receives a command and initiates the corresponding action
*/
executeCmd: function(cmd) {
switch (cmd.action) {
case "initialize":
this.p = new Process(this);
break;
case "process":
this.p.run(cmd.i);
break;
}
},
/*
* Places next command into the command queue
*/
nextInput: function() {
this.cmdQueue.enqueue({
action: "process",
i: this.i++
});
}
}
// The modified loop-like process
var Process = function(gui) {
this.gui = gui;
}
Process.prototype.run = function(i) {
// The workload from the original process above
console.log(i);
// The loop itself is controlled by the GUI
if (this.gui.carryOn) {
this.gui.nextInput();
}
}
// Event handlers for GUI interaction
window.onload = function() {
var gui = new Gui(100);
document.getElementById("init").onclick = function() {
gui.cmdQueue.enqueue({ // first command will instantiate the process
action: "initialize"
});
// Periodically check the command queue for commands
gui.timer = setInterval(function() {
if (gui.cmdQueue.peek() !== undefined) {
gui.executeCmd(gui.cmdQueue.dequeue());
}
}, 4);
}
document.getElementById("step").onclick = function() {
gui.carryOn = false; // execute just one step
gui.nextInput();
}
document.getElementById("run").onclick = function() {
gui.carryOn = true; // (restart) and execute until further notice
gui.nextInput();
}
document.getElementById("pause").onclick = function() {
gui.carryOn = false; // pause execution
}
document.getElementById("stop").onclick = function() {
gui.carryOn = false; // stop execution and clean up
gui.i = gui.start;
clearInterval(gui.timer)
while (gui.cmdQueue.peek()) {
gui.cmdQueue.dequeue();
}
}
}
</script>
</head>
<body>
<input id="init" type="button" value="Init" />
<input id="step" type="button" value="Step" />
<input id="run" type="button" value="Run" />
<input id="pause" type="button" value="Pause" />
<input id="stop" type="button" value="Stop" />
</body>
</html>
While this approach certainly doesn't fit all long-running scripts one can think of, it certainly
can be adapted to any loop-like scenario. I'm using it to port Numenta's HTM/CLA artificial
intelligence algorithms to the browser.
Related
I have a web page where a javascript calculation in a function takes lot of time to finish and makes the page to freeze. What technique should I use to make sure the javascript does not freeze the browser when the calculation is happening in the background?
If you only need to do a calculation and don't need to access the DOM during the long running calculation, then you have two options:
You can break the calculation up into pieces and do a piece at a time on a setTimeout(). On each setTimeout() call, the browser will be free to serve other events and will keep the page alive and responive. When you finish the last piece of the calculation, you can then carry out the result.
You can run the calculation in the background using a webworker in modern browsers. When the calcuation is done in the webworker, it sends a message back to the main thread and you can then update the DOM with the result.
Here's a related answer that also shows an example: Best way to iterate over an array without blocking the UI
Let me elaborate on #jfriend00's answer by giving a concrete stripped down example. Here is a long-running JavaScript process that can be started by clicking a button. Once it runs, it freezes the browser. The process consists of a long loop that repeats some workload where one iteration takes comparatively little time.
Due to the browser freeze, debugging a script like this is not easy. One alternative to avoid browser freeze is using a web worker. The drawback of that approach is the poor debuggabilty of web workers per se: Tools like Firebug are not supported.
<html>
<head>
<script>
var Process = function(start) {
this.start = start;
}
Process.prototype.run = function(stop) {
// Long-running loop
for (var i = this.start; i < stop; i++) {
// Inside the loop there is some workload which
// is the code that is to be debugged
console.log(i);
}
}
var p = new Process(100);
window.onload = function() {
document.getElementById("start").onclick = function() {
p.run(1000000000);
}
}
</script>
</head>
<body>
<input id="start" type="button" value="Start" />
</body>
</html>
Using a Queue data structure (e.g. http://code.stephenmorley.org/javascript/queues/), an interval timer and some small modification to the control flow of the original process one can build a GUI that doesn't freeze the browser, leaves the process fully debuggable and even allows additional features
like stepping, pausing and stopping.
Here is how it goes:
<html>
<head>
<script src="http://code.stephenmorley.org/javascript/queues/Queue.js"></script>
<script>
// The GUI controlling process execution
var Gui = function(start) {
this.timer = null; // timer to check for inputs and/or commands for the process
this.carryOn = false; // used to start/pause/stop process execution
this.cmdQueue = new Queue(); // data structure that holds the commands
this.p = null; // process instance
this.start = start;
this.i = start; // input to the modified process
}
Gui.prototype = {
/**
* Receives a command and initiates the corresponding action
*/
executeCmd: function(cmd) {
switch (cmd.action) {
case "initialize":
this.p = new Process(this);
break;
case "process":
this.p.run(cmd.i);
break;
}
},
/*
* Places next command into the command queue
*/
nextInput: function() {
this.cmdQueue.enqueue({
action: "process",
i: this.i++
});
}
}
// The modified loop-like process
var Process = function(gui) {
this.gui = gui;
}
Process.prototype.run = function(i) {
// The workload from the original process above
console.log(i);
// The loop itself is controlled by the GUI
if (this.gui.carryOn) {
this.gui.nextInput();
}
}
// Event handlers for GUI interaction
window.onload = function() {
var gui = new Gui(100);
document.getElementById("init").onclick = function() {
gui.cmdQueue.enqueue({ // first command will instantiate the process
action: "initialize"
});
// Periodically check the command queue for commands
gui.timer = setInterval(function() {
if (gui.cmdQueue.peek() !== undefined) {
gui.executeCmd(gui.cmdQueue.dequeue());
}
}, 4);
}
document.getElementById("step").onclick = function() {
gui.carryOn = false; // execute just one step
gui.nextInput();
}
document.getElementById("run").onclick = function() {
gui.carryOn = true; // (restart) and execute until further notice
gui.nextInput();
}
document.getElementById("pause").onclick = function() {
gui.carryOn = false; // pause execution
}
document.getElementById("stop").onclick = function() {
gui.carryOn = false; // stop execution and clean up
gui.i = gui.start;
clearInterval(gui.timer)
while (gui.cmdQueue.peek()) {
gui.cmdQueue.dequeue();
}
}
}
</script>
</head>
<body>
<input id="init" type="button" value="Init" />
<input id="step" type="button" value="Step" />
<input id="run" type="button" value="Run" />
<input id="pause" type="button" value="Pause" />
<input id="stop" type="button" value="Stop" />
</body>
</html>
While this approach certainly doesn't fit all long-running scripts one can think of, it certainly
can be adapted to any loop-like scenario. I'm using it to port Numenta's HTM/CLA artificial
intelligence algorithms to the browser.
Some browsers have only one thread for running your code and updating the UI (in other words, until the calculation is complete, the browser will appear "frozen"). You'll want to try to perform the action asynchronously, in one way or another.
If the calculation is really expensive, you might want to make a call to the server and let the server do the calculation, and callback the client when the calculation is done.
If the calculation is kind of expensive, you can try to do it in chunks on the client. This isn't actually asynchronous (as the client will block while executing each chunk) but the goal is to make the chunks small enough that the blocking is not noticeable.
setTimeout(function() { ..code }, 0);
I recommend this for heavy execution time, and also for on load ajax you could try to add
$(window).on("load", function (e) { }); // for jquery v3
if its in the loading process.
I think this should resolve your problem,
function myClickOperation(){
var btn_savebutton2 = document.querySelector("input[id*='savebutton2']");
setTimeout(function () { btn_savebutton2.click() }, 1000);
}
// Full Html content
<html>
<script>
function myClickOperation(){
var btn_savebutton2 = document.querySelector("input[id*='savebutton2']");
document.getElementById('savebutton1').disabled = true;
setTimeout(function () { btn_savebutton2.click() }, 1000);
}
function testClick(){
var idd = document.getElementById("myid");
idd.innerHTML =idd.innerHTML +"<br/>" + new Date();
if(true){
setTimeout(function () { testClick() }, 1);
}
}
</script>
<body>
<input type="button" id="savebutton1" onclick="myClickOperation()" value="Click me" />
<input type="button" id="savebutton2" onclick="testClick()" value="Do not click this" />
<input type="text"/>
<input type="button" value="temp"/>
<div style="height: 300px;overflow-y: scroll;" id="myid"/>
</body>
How a user can stop a loop by clicking a button
Here what I need to do in my loop :
HTML :
<input type="button" id="stop" value="Stop" />
<input type="button" id="go" value="Go" />
<div id="div"> </div>
Javascript :
document.getElementById('go').addEventListener('click', function(){
// request to server that return a list of object.
// for each returned objet, I do another request to the server. like that
for(let i=0; i < objets.length; i++){
// request to the server.
// update the div for progress bar like 1%..2%
}
});
document.getElementById('stop').addEventListener('click', function(){
// how to break the loop made by go button
});
If you're running a simple for (..) loop, this cannot be stopped via external influence. Everything is happening on the same thread in Javascript, unless your code "ends" at some point and returns control to the browser for a while no UI interaction can happen. The easiest way to have a "loop" is via a setTimeout or setInterval:
interval = null;
startBTN.onclick = function () {
var i = 0;
interval = setInterval(function () {
console.log(i++); // this is inside your loop
}, 1);
};
stopBTN.onclick = function () {
clearInterval(interval);
};
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:
How to change the code to make it really assyncronous and
responsive.
Why setTimeout() is not working if it should work according the Web Worker definition.
Thanks,
Rafael Afonso
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);
}
}
albanx:
Following your suggestion, I changed the worker.js code like this:
var running = false;
var stepTime = 0;
function run() {
if(running) {
postMessage({msg: "TIME", time: new Date().toISOString()});
setTimeout(run, stepTime);
}
}
self.addEventListener('message', function(e) {
switch(e.data.msg) {
case "START":
running = true;
console.debug("[WORKER] STARTING. running = " + running);
postMessage({msg: "STARTED"});
run();
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);
}
});
This time all worked as expected: the Stop button was enabled and really stopped the loop. Also It was possible change the step time changing the <SELECT>. Finally, it was possible restart the loop so many times I wanted.
My only question is: Don´t these recrsive calls to run (setTimeout(run, stepTime);) can create a stack overflow?
Thanks,
Rafael Afonso
I'm trying to solve a quite simple task but stuck with JQuery behavior.
I have a HTML button which I disable (add disabled attribute) right after it get clicked to prevent multiple clicks, do something long running (i.e. update DOM with a lot of elements) and enable the button back.
Problem is that even the button is disabled jquery queues all clicks on it and raise my click handler right after it became enabled.
According to JQuery docs it should not raise events for a disabled element.
Bellow is my code. Open JS console, click two times on the button, notice couple 'START --' messages in the console.
<div>
<button id="mybtn" type="button">Find</button>
</div>
var btnElement = $('#mybtn');
btnElement.click(doClick);
function doClick() {
var btnElement = $('#mybtn');
btnElement.attr('disabled', true);
console.log('START --' + Date());
for (var i = 0; i < 70000; i++) {
var el = $('#mybtn');
var w = el.width();
w += 10;
}
console.log('STOP --' + Date());
el.attr('disabled', false);
}
Here is my solution http://jsfiddle.net/DRyxd/8/
var btnElement = $('#mybtn');
var buttonIsBusy = false;
function doHeavyJob () {
console.log('START --' + Date());
for (var i = 0; i < 70000; i++) {
var el = $('#mybtn');
var w = el.width();
w += 10;
}
var timeoutId = setTimeout (unblockTheButton, 0);
console.log('STOP --' + Date());
}
function unblockTheButton () {
console.log('unblockTheButton');
btnElement.attr('disabled', false);
buttonIsBusy = false;
}
function doClick() {
console.log('click', buttonIsBusy);
if (buttonIsBusy) {
return;
}
btnElement.attr('disabled', true);
buttonIsBusy = true;
var timeoutId = setTimeout (doHeavyJob, 0);
}
btnElement.click(doClick);
The issue here is that click-handler function has not finished and browser has not refreshed the DOM. That means that block was not yet applied to the button. You can try pushing your heavy code out of the current context like this:
function someHeavyCode () {
/* do some magic */
}
var timeoutId = setTimeout(someHeavyCode, 0);
This will push your heavy code out of the current context.Letting browser to update the DOM first and only after execute the heavy code.
While the heavy code is executed, browser (at least Chrome) kept the user input queue somewhere in other place (or most-likely other thread). And as soon as heavy code completes - it feeds the DOM with all that queued events. We need to ignore that events somehow. And I use the setTimeout with 0-time again. Letting the browser do what was queued before unblocking the button.
WARNING But be extremely careful with this technique. Browser will still be blocked and if you spawn a lot of such blocks it may hang.
See also this Why is setTimeout(fn, 0) sometimes useful? and consider using webworkers.
P.S. Blocking a user input in such a way is not a good approach, try to rethink what you are going to do, probably there is a better solution for that.
I have the following code:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<div id="logger"></div>
<script>
function log(txt) {
document.getElementById('logger').innerHTML += txt + '<br>';
}
var int = 10;
var a= setTimeout(function(){
a = null;
log("A fired!");
clearTimeout(b);
b = null;
}, int);
var b = setTimeout(function(){
b = null;
log("B fired!");
clearTimeout(a);
a = null;
}, int);
</script>
</body>
</html>
Both timeout callbacks should prevent another another one from firing. In Opera, FF and Chrome only first one (that prints ″A fired″) is executed. But when I run the same code in IE6 and IE8, both callbacks are executed. Is that some error in my scriupt or is that one of those bugs that these browsers are full of? Do clearTimeout()/clearInterval() guarantee that callback won't be called after their invocation?
I think what is happening is that:
JavaScript has an event queue.
IE processes the timeouts, and queues two events.
The first timeout event is processed, and clearTimeout is called for B. However the event for B is already queued, so it still gets fired.
the second timeout event is processed, and clearTimeout is called for A.
I suspect that in IE, the event gets queued and calling clearTimeout does not remove the event from the event queue.
It is also possible there is just funkyness in how IE pushes simultaneous timeouts onto the queue... Diagnosing the underlying cause could be acheived by using two different timeouts, using 100% CPU processing loops for x time, and by queuing/slotting in other events (maybe can inject events into queue using window.postMessage() and catch them with window.onMessage()).
I have modified your existing code to demonstrate the problem better. It queues the log items rather than doing them immediately, because calling display() can cause layout or rendering to occur, which can easily introduce other funky interference.
Edit: You can test this using http://jsbin.com/ucukez/2 - if the browser has the fault then you get "in A timeout fired" and "in B timeout fired".
Edit: This was fixed in IE9 - I couldn't reproduce in IE9/IE10/IE11.
The HTML is:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>setTimeout queued test</title>
<script>
function display(txt) {
document.getElementById('logger').innerHTML += txt + '<br>';
}
var log = {
items: [],
push: function(text) {
this.items.push(text);
},
display: function() {
var items = this.items;
this.items = [];
for (var i = 0; i < items.length; i++) {
display(items[i]);
}
}
};
function startTest() {
var ms = 10;
display("startTest()");
log.push('before A setTimeout');
var a = setTimeout(function(){
log.push('in A timeout fired');
display("A fired!");
log.push('in A clear timer B');
clearTimeout(b);
log.push('in A cleared timer B');
}, ms);
log.push('after A setTimeout');
log.push('before B setTimeout');
var b = setTimeout(function(){
log.push('in B timeout fired');
display("B fired!");
log.push('in B clear timer A');
clearTimeout(a);
log.push('in B cleared timer A');
}, ms);
log.push('after B setTimeout');
setTimeout(function(){
display("");
display("Displaying logged items:");
log.display();
},1000);
}
</script>
</head>
<body onload="startTest()">
<div id="logger"></div>
</body>
</html>
You're setting the timeouts to occur at exactly the same time, and since they are both forked processes you get inconsistent results
Your best bet is to first test to see if the timeout is still valid like this:
var int = 10;
var a= setTimeout(function(){
if (!a) return;
a = null;
log("A fired!");
clearTimeout(b);
b = null;
}, int);
var b = setTimeout(function(){
if (!b) return;
b = null;
log("B fired!");
clearTimeout(a);
a = null;
}, int);