Skip to content

Instantly share code, notes, and snippets.

@subzey
Last active August 29, 2015 14:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save subzey/e0c242780f5a46e2c6f3 to your computer and use it in GitHub Desktop.
Save subzey/e0c242780f5a46e2c6f3 to your computer and use it in GitHub Desktop.
Too Asynchronous

Too Asynchronous

Asynchronous JS in Firefox (37.0.2) is broken. I mean it.

It started during routine debugging: something went wrong with form validation and I've stopped at breakpoint in Firefox debugger. And then I saw something that amazed me. The financial quotes on the page (taken from WebSocket live stream) were updating. While I was standing on a break-point and main event loop should have been stopped something updated the DOM.

It doesn't look like plain old "one thread with event loop" JavaScript. So I had to investigate that.

Quirky debugger

I made a test. Various asynchronous stuff is started: XHR, WebWorker, postMessage, WebSocket and plain old setTimeout, each logging a text line into a <pre> upon completion. And finally an ordinary synchronous expression.

Here's the code:

function _log(message){
	// Helper function, appends text into a page
	document.querySelector('pre').textContent += message + '\n';
}

function doAsyncStuff(){
	// XHR
	var xhr = new XMLHttpRequest();
	xhr.open('GET', 'samplexhr.txt', true);
	xhr.onreadystatechange = function(){
		if (xhr.readyState === 4){
			_log('Async: XHR');
		}
	};
	xhr.send();

	// WebWorker
	var worker = new Worker('sampleworker.js');
	worker.onmessage = function(){
		_log('Async: WebWorker');
	};

	// PostMessage
	window.addEventListener('message', function(e){
		_log('Async: postMessage');
	}, false);
	window.postMessage('Oh, hi!', '*');

	// WebSocket
	var ws = new WebSocket("ws://echo.websocket.org/");
	ws.onopen = function() {
		_log('Async: Websocket');
		ws.close();
	};

	// setTimeout
	setTimeout(function(){
		_log('Async: setTimeout');
	}, 0);
}

doAsyncStuff();

// Just write into console
_log('Synchronous');

If you're an experienced JS developer, you probably can tell what should be printed into <pre>.

First line should be"Synchronous" and all the others should be printed below because they're… ahem… asynchronous. Note that "async" lines can be printed in any particular order: as soon as browser unpredictably decides the process is done.

So we create all the files (sampleworker.js and samplexhr.txt) and then run the code above in Firefox and get the result.

All the files mentioned are right here in this gist, you can take it and try it yourself. These files should be served via HTTP, some tests, likepostMessage, won't work if index.html is opened directly from disk.

Synchronous
Async: postMessage
Async: setTimeout
Async: XHR
Async: WebWorker
Async: Websocket

Just as expected! But now we'll set a breakpoint just before the synchronous log call, like this:

doAsyncStuff();

debugger;

// Just write into console
_log('Synchronous');

The contents of doAsyncStuff function wasn't changed

Script execution almost immediately stops at the breakpoint. And we get the following on the screen:

Async: postMessage
Async: XHR
Async: Websocket

Then we resume the script execution and following lines are appended:

Synchronous
Async: WebWorker
Async: setTimeout

The heck is that? postMessage, XHR and Websocket just jumped the queue leaving unfinished synchronous code behind. The scripts runs quite the opposite way it should, just because debugger is enabled.

This is not how debuggers work, right, Mozilla?

A broader issue

Maybe it's not a debugger issue, but the way Mozilla erroneously optimize the event loop?

Busy loop

Let's try a busy loop, an artificial delay big enough for async stuff to finish:

doAsyncStuff();

var targetTimestamp = Date.now() + 5000;
while (Date.now() < targetTimestamp){
	// Do nothing
}

_log('Synchronous');

Works as charm. Firefox freezes for 5 seconds, then spitting out lines in a correct order.

Synchronous
Async: postMessage
Async: setTimeout
Async: XHR
Async: WebWorker
Async: Websocket

Synchronous XHR

Now let's try another event loop blocker, a synchronous XHR. 100 times, just to be sure it's enough:

doAsyncStuff();

for (var i=0; i<100; i++){
	var xhr = new XMLHttpRequest();
	xhr.open('GET', 'samplexhr.txt', false); // Synchronous
	xhr.send();
}

_log('Synchronous');

The result:

Async: postMessage
Async: XHR
Async: Websocket
Synchronous
Async: WebWorker
Async: setTimeout

Yay! Same incorrect behavior again. And no debugger is involved at this time.

alert

And the last, the most old and infamous method, an alert(). Here's the code:

doAsyncStuff();

alert('alert() is synchronous!');

_log('Synchronous');

Here's a result. Alert dialog is shown and the text on a page reads:

Async: postMessage
Async: XHR
Async: WebWorker
Async: Websocket

Then after OK button is pressed two lines appears:

Async: setTimeout
Synchronous

This time synchronous code ran the last, even setTimeout somehow managed to outrun it.

Previous results were incorrect, this one is absolutely wrong.

Conclusion

Of course, it is quite unlikely one can encounter alert() or synchronous XHR in the wild these days. But it would be great if this stuff (although deprecated) didn't ruin the program flow.

But even if you don't want to use these outdated anti-patterns (and you shouldn't), it would be great... no, it's absolutely necessary for debugger to work in an expected way.

Bonus

I've got something about async execution for Chrome, too.

setTimeout(function(){
	console.log(1);
}, 10);

setTimeout(function(){
	console.log(2);
}, 5);

How should this code work?

Second setTimeout has lower timeout value, so it should run first, even if main loop is blocked for more than 10 seconds. So the result should be:

2
1

It works everywhere, with one exception. In Chrome when page is not focused, it is called in the order timeouts were defined:

1
2
<!DOCTYPE html>
<html lang="en">
<head>
<title>Async test</title>
<meta charset="utf-8">
</head>
<body>
<pre></pre>
<script>
function _log(message){
document.querySelector('pre').textContent += message + '\n';
}
function doAsyncStuff(){
// XHR
var xhr = new XMLHttpRequest();
xhr.open('GET', 'samplexhr.txt', true);
xhr.onreadystatechange = function(){
if (xhr.readyState === 4){
_log('Async: XHR');
}
};
xhr.send();
// WebWorker
var worker = new Worker('sampleworker.js');
worker.onmessage = function(){
_log('Async: WebWorker');
};
// PostMessage
window.addEventListener('message', function(e){
_log('Async: postMessage');
}, false);
window.postMessage('Oh, hi!', '*');
// WebSocket
var ws = new WebSocket("ws://echo.websocket.org/");
ws.onopen = function() {
_log('Async: Websocket');
ws.close();
};
// plain old setTimeout
setTimeout(function(){
_log('Async: setTimeout');
}, 0);
}
doAsyncStuff();
alert('alert() is synchronous!');
_log('Synchronous');
</script>
</body>
</html>
postMessage('Oh, hi!');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment