Disabling JIT in Safari 6 to workaround severe Javascript JIT bugs - javascript

We found a severe problem with the interpretation of our Javascript code that only occurs on iOS 5/Safari 6 (then current iPad release) that we think is due to critical bug in the Just in Time JS compiler in Safari. (See updates below for more affected versions and versions that seem to now contain a fix).
We originally found the issue in our online demos of our library: the demos crash more or less randomly but this happens only the second time (or even later) that the same code is executed. I.e. if you run the part of the code once, everything works OK, however subsequent runs crash the application.
Interestingly executing the same code in Chrome for iOS the problem does not show, which we believe is due to the missing JIT capabilities of the Webview that is used in Chrome for iOS.
After a lot of fiddling we finally think we found at least one problematic piece of code:
var a = 0; // counter for index
for (var b = this.getStart(); b !== null; b = b.getNext()) // iterate over all cells
b.$f = a++; // assign index to cell and then increment
In essence this is a simple for loop that assigns each cell in a linked list data structure its index. The problem here is the post-increment operation in the loop body. The current count is assigned to the field and updated after the expression is evaluated, basically the same as first assigning a and then incrementing it by one.
This works OK in all browsers we tested and in Safari for the first couple of times, and then suddenly it seems as if the counter variable a is incremented first and then the result is assigned, like a pre-increment operation.
I have created a fiddle that shows the problem here: http://jsfiddle.net/yGuy/L6t5G/
Running the example on an iPad 2 with iOS 6 and all updates the result is OK for the first 2 runs in my case and in the third identic run suddenly the last element in the list has a value assigned that is off by one (the output when you click the "click me" button changes from "from 0 to 500" to "from 0 to 501")
Interestingly if you switch tabs, or wait a little it can happen that suddenly the results are correct for two or so more runs! It seems as if Safari sometimes resets is JIT caches.
So since I think it may take a very long for the Safari team to fix this bug (which I have not yet reported) and there may be other similar bugs like this lurking in the JIT that are equally hard to find, I would like to know whether there is a way to disable the JIT functionality in Safari. Of course this would slow down our code (which is very CPU intensive already), but better slow than crashing.
Update:
Unsurprisingly it's not just the post increment operator that is affected, but also the post decrement operator. Less surprisingly and more worryingly is that it makes no difference if the value is assigned, so looking for an assignment in existing code is not enough. E.g. the following the code b.$f = (a++ % 2 == 0) ? 1 : 2; where the variables value is not assigned but just used for the ternary operator condition also "fails" in the sense that sometimes the wrong branch is chosen. Currently it looks as if the problem can only be avoided if the post operators are not used at all.
Update:
The same issue does not only exist in iOS devices, but also on Mac OSX in Safari 6 and the latest Safari 5:
These have been tested and found to be affected by the bug:
Mac OS 10.7.4, Safari 5.1.7
Mac OS X 10.8.2, WebKit Nightly r132968: Safari 6.0.1 (8536.26.14, 537+). Interestingly these do not seem to be affected: iPad 2 (Mobile) Safari 5.1.7, and iPad 1 Mobile Safari 5.1. I have reported these problems to Apple but have not received any response, yet.
Update:
The bug has been reported as Webkit bug 109036. Apple still has not responded to my bug report, all current (February 2013) Safari versions on iOS and MacOS are still affected by the problem.
Update 27th of February 2013:
It seems the bug has been fixed by the Webkit team here! It was indeed a problem with the JIT and the post-operators! The comments indicate that more code might have been affected by the bug, so it could be that more mysterious Heisenbugs have been fixed, now!
Update October 2013:
The fix finally made it into production code: iOS 7.0.2 at least on iPad2 does not seem to suffer from this bug anymore. I did not check all of the intermediate versions, though, since we worked around the problem a long time ago.

Try-catch blocks seem to disable the JIT compiler on Safari 6 on Lion for the part directly inside the try block (this code worked for me on Safari 6.0.1 7536.26.14 and OS X Lion).
// test function
utility.test = function(){
try {
var a = 0; // counter for index
for (var b = this.getStart(); b !== null; b = b.getNext()) // iterate over all cells
b.$f = a++; // assign index to cell and then increment
}
catch (e) { throw e }
this.$f5 = !1; // random code
};
This is at least a documented behavior of the current version of Google's V8 (see the Google I/O presentation on V8), but I don't know for Safari.
If you want to disable it for the whole script, one solution would be to compile your JS to wrap every function's content inside a try-catch with a tool such as burrito.
Good job on making this reproducible!

IMO, the correct solution is to report the bug to Apple, then workaround it in your code (surely using a separate a = a + 1; statement will work, unless the JIT is even worse than you thought!). It does indeed suck, though. Here's a list of common things you can also try throwing in to the function to make it de-optimise and not use JIT:
Exceptions
'with' statement
using arguments object, e.g. arguments.callee
eval()
The problem with those is if the Javascript engine is optimised to JIT them before they fix that bug, in which case you're back to crashing. So, report and workaround!

Actually, the FOR loop bug is still present in Safari on iOS 7.0.4 in iPhone 4 and iPad 2. The loop failing can be significantly simpler than the illustration above, and it rakes several passes through the code to hit. Changing to a WHILE loop allows proper execution.
Failing code:
function zf(num,digs)
{
var out = "";
var n = Math.abs(num);
for (digs; digs>0||n>0; digs--)
{
out = n%10 + out;
n = Math.floor(n/10);
}
return num<0?"-"+out:out;
}
Successful code:
function zf(num,digs)
{
var out = "";
var n = Math.abs(num);
do
{
out = n%10 + out;
n = Math.floor(n/10);
}
while (--digs>0||n>0)
return num<0?"-"+out:out;
}

Related

Mysterious failure of jQuery.each() and Underscore.each() on iOS

A brief summary for anyone landing here from Google: There is a bug in iOS8 (on 64-bit devices only) that intermittently causes a phantom "length" property to appear on objects that only have numeric properties. This causes functions such as $.each() and _.each() to incorrectly try to iterate your object as an array.
I have filed an issue report (really a workaround request) with jQuery (https://github.com/jquery/jquery/issues/2145), and there is a similar issue on the Underscore tracker (https://github.com/jashkenas/underscore/issues/2081).
Update: This is a confirmed webkit bug. A fix was comitted on 2015-03-27, but there is no indication as to which version of iOS will have the fix. See https://bugs.webkit.org/show_bug.cgi?id=142792. Currently iOS 8.0 - 8.3 are known to be affected.
Update 2: A workaround for the iOS bug can be found in jQuery 2.1.4+ and 1.11.3+ as well as Underscore 1.8.3+. If you're using any of these versions, then the library itself will behave properly. However, it's still up to you to ensure that your own code isn't affected.
This question can also be called: "How can an object without a length have a length?"
I'm having a twilight zone kind of issue with mobile Safari (seen on both iPhones and iPads running iOS 8). My code has a lot of intermittent failures using the "each" implementation of both jQuery ($.each()) and Underscore (_.each()).
After some investigation, I discovered that in all cases of failure, the each function was treating my object as an array. It would then try to iterate it like an array (obj[0], obj[1], etc.) and would fail.
Both jQuery and Underscore use the length property to determine if an argument is an object or an array/array-like collection. For example, Underscore uses this test:
if (length === +length) { ... this is an array
My objects had no length parameter, yet they were triggering the above if statements. I double validated that there was no length by:
Sending the value of obj.length to the server for logging prior to calling each() (confirming that length was undefined)
Calling delete obj.length prior to calling each() (this didn't change anything.)
I have finally been able to capture this behavior in the debugger with an iPhone attached to Safari on a Mac.
The following picture shows that $.isArrayLike thinks that length is 7.
However, a console trace shows that length is undefined, as expected:
At this point I believe this is a bug in iOS Safari, especially since it's intermittent. I'd love to hear from others who's seen this problem and perhaps found a way to counter it.
Update
I was asked to create a fiddle of this, but unfortunately I can't. There seems to be a timing issue (which may even differ between devices) and I can't reproduce it in a fiddle. This is the minimum set of code I was able to repro the problem with, and it requires an external .js file. With this code happens 100% of the time on my iPhone 6 running 8.1.2. If I change anything (e.g. making the JS inline, removing any of the unrelated JS code, etc), the problem goes away.
Here is the code:
index.html
<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="script.js"></script>
</head>
<body>
Should say 3:
<div id="res"></div>
<script>
function trigger_failure() {
var obj = { 1: '1', 2: '2', 3: '3' };
print_last(obj);
}
$(window).load(trigger_failure);
</script>
</body>
</html>
script.js
function init_menu()
{
var elemMenu = $('#menu');
elemMenu
.on('mouseenter', function() {})
.on('mouseleave', function() {});
elemMenu.find('.menu-btn').on('touchstart', function(ev) {});
$(document).on('touchstart', function(ev) { });
return;
}
function main_init()
{
$(document).ready(function() {
init_menu();
});
}
function print_last(obj)
{
var a = $($.parseHTML('<div></div>'));
var b = $($.parseHTML('<div></div>'));
b.append($.parseHTML('foo'));
$.each(obj, function(key, btnText) {
document.getElementById('res').innerHTML = ("adding " + btnText);
});
}
main_init();
This isn't an answer but rather an analysis of what's going on under the covers after much testing. I hope that, after reading this, someone on either safari mobile side or the JavaScript VM on iOS side can take a look and correct the issue.
We can confirm that the _.each() function is treating js objects {} as arrays [] because the safari browser returns the 'length' property of an object as an integer. BUT ONLY IN CERTAIN CASES.
If we use an object map where the keys are integers:
var obj = {
23:'some value',
24:'some value',
25:'some value'
}
obj.hasOwnProperty('length'); //...this comes out as 'false'
var length = obj.length; //...this returns 26!!!
Inspecting with the debugger on mobile Safari browser we clearly see that obj.length is "undefined". However stepping to next line:
var length = obj.length;
the length variable is clearly being assigned the value 26 which is an integer. The integer part is important because the bug in underscore.js occurs at these two lines in the underscore.js code:
var i, length = obj.length;
if (length === +length) { //... it treats an object as an array because
//... it has assigned the obj (which has no own
//... 'length' property) an actual length (integer)
However if we were to change the object in question just slightly and add a key-value pair where the key is a string (and is the last item in object) such as:
var obj = {
23:'some value',
24:'some value',
25:'some value',
'foo':'bar'
}
obj.hasOwnProperty('length'); //...this comes out as 'false'
var length = obj.length; //...this now returns 'undefined'
More interestingly, if we change the object again and a key-value pair such as:
var obj = {
23:'some value',
24:'some value',
25:'some value',
75:'bar'
}
obj.hasOwnProperty('length'); //...this comes out as 'false'
var length = obj.length; //...this now returns 76!
It appears that the bug (wherever it is happening: Safari/JavaScript VM) looks at the key of last item in the object and if it is an integer adds one (+1) to it and reports that as a length of the object...even though obj.hasOwnProperty('length') comes back as false.
This occurs on:
some iPads (but NOT ALL that we have) with iOS version 8.1.1, 8.1.2, 8.1.3
the iPads that it does occur on, it happens consistently...every time
only Safari browser on iOS
This does not occur on:
any iPhones we tried with iOS 8.1.3 (using both Safari and Chrome)
any iPads with iOS 7.x.x (using both Safari and Chrome)
chrome browser on iOS
any js fiddles we attempted to create using the above mentioned iPads that consistently created the error
Because we can't really prove it with a jsFiddle we did the next best thing and got a screen capture of it stepping through the debugger. We posted the video on youTube and it can be seen at this location:
https://www.youtube.com/watch?v=IR3ZzSK0zKU&feature=youtu.be
As stated above this is just an analysis of the problem in more detail. We are hoping someone with more understanding under the hood can comment on the situation.
One simple solution is to NOT USE _.each function (or the jQuery equivalent). We can confirm that using angular.js forEach function remedies this issue. However, we use underscore.js pretty extensively and _.each is used nearly everywhere we iterate through arrays/collections.
Update
This was confirmed as a bug and there is now a fix for this bug on WebKit as of 2015-03-27:
fix: http://trac.webkit.org/changeset/182058
original bug report: https://bugs.webkit.org/show_bug.cgi?id=142792
For anyone looking at this and using jQuery, just a heads' up that this has now been fixed in versions 2.1.4 and 1.11.3, which specifically only contain a hot-fix to the above issue:
http://blog.jquery.com/2015/04/28/jquery-1-11-3-and-2-1-4-released-ios-fail-safe-edition/
Upgrading to the latest version of lodash (3.10.0) fixed this problem for me.
Take note that there is a breaking change in this lodash version with _.first vs _.take.
For anyone who isn't familiar with lodash - it's a fork of underscore that is nowadays (imo) a better solution.
Really a big thanks to #OzSolomon for explaning, describing and figuring out this problem.
If I would be able to give a bounty to a question I would've done it.
I have worked a while with something that could be similar or the same problem. The company I work for have a game that uses KineticJS 4.3.2. Kinetic makes intensive use of Arrays when grouping graphic objects on a html5 canvas.
The problems that occurred was that push was missing on the array sometimes or that properties on the object that was stored in the array was missing. The problem occurred in Safari and when run from homescreen in iOS8. For some reason the problem did not occur when run in Chrome on iOS. The problem also did not occur with a debugger connected.
After a lot of testing and searching on the net we think this is a JIT optimisation bug in webkit on iOS. A colleague found the following links.
TypeError: Attempted to assign to readonly property. in Angularjs application on iOS8 Safari
https://github.com/angular/angular.js/issues/9128
http://tracker.zkoss.org/browse/ZK-2487
Currently we have made a workaround by removing dot-notation when accessing the array that did not work (.children was replaced with [ "children" ]).
I have not been able to create a jsFiddle.
The problem is the user code, not iOS8 webkit.
var obj = {
23: 'a',
24: 'b',
25: 'c'
}
The map keys above are integers, when strings should be used. It's not a valid map by the javascript standard so the behaviour is undefined, and Apple is choosing to interpret `obj' as an array of 26 elements (indexes 0 to 25 inclusive).
Use string keys and the length problem will go away:
var obj = {
'23': 'a',
'24': 'b',
'25': 'c'
}

Is there any way to force the Javascript Garbage Collector in webkit based browsers?

In internet explorer we can force the Javascript garbage collection to execute with this method: CollectGarbage();
That method is undefined on Firefox. Do you know if there is some kind of equivalent?
Thanks.
(Not just limiting this answer to WebKit-based browsers...)
Chrome: if you launch it from a command line/terminal with --js-flags="--expose-gc", then it provides window.gc().
Firefox I think requires clicking the "Free memory" buttons in about:memory.
Opera has window.opera.collect().
Edge has window.CollectGarbage().
Safari, unknown.
Note that you shouldn't be manually running the GC. I've only posted this because it's useful for development testing.
Visit about:memory.
From page's documentation on MDN:
about:memory is a special page within Firefox that lets you view, save,
load, and diff detailed measurements of Firefox's memory usage. It also
lets you do other memory-related operations like trigger GC and CC
I've been just trying to force GC and it seems that regardless of the actual browser relatively good way of doing so is to run following piece of code:
function gc(max) {
var arr = [];
for (var i = 0; i < max; i++) {
arr.push(i);
}
return arr.length;
}
for (var i = 0; ; i++) {
// repeat until you have enough:
gc(Math.pow(2, i));
}
Of course, one issue is to know when to stop calling gc(...): for that one needs some external way to detect the GC is successfully over. In my case I was using embedded WebView and I could really check before forcing next round with bigger array.
I'm not sure if this is not off topic but there is add-on for firefox called FreeMemory (https://addons.mozilla.org/en-US/firefox/addon/freememory/) to run garbage or cycle collection without visiting about:memory pane, with configurable timer. I believe there are alternatives for other browsers out there.

Old JavaScript Function Crashes Browser

I'm integrating a mootools script onto a page which has very old JavaScript functions which run a navigation vertical menu. This old script will be hard to change now.
The line breaking is:
function stgobj(id) {
with(document) return nIE && nVER < 5 ? all[id] : nNN4 ? layers[id] : getElementById(id);
}
Not sure exactly what's it's purpose, but it looks like it's rendering some elements. If commented the menu will disappear.
FF, Chrome, IE(doesn't crash, but menu does not render)
Any quick patch to resolve the browsers crashing?
Looks like its purpose is to return the element corresponding to the given ID. The code simply uses some different methods based on the browser - document.all for IE5 and earlier, and document.layers for Netscape 4. Unless you need to support those ancient browsers, you could alter the function to return just document.getElementById(id). Or better yet, ditch this function altogether and call document.getElementById directly.
However, if it's crashing modern browsers like Firefox and Chrome, then you should also look at the browser detection logic (the code that populates the nIE, nVER and nNN4 variables), otherwise it might just end up crashing elsewhere.
It's a "compatibility" function for document.getElementById. I think you should be able to equal it:
stgobj = document.getElementById.bind(document);

undefined behaviour with an un initialised member variable in javascript

It's 20:30 and I'm after a 6 hour bug hunt of an irritating bug caused by an uninitialized member variable.
In the our previous version we had the next few lines of code:
var aList = new Array;
for (var iDx=0; iDx < nNumOfElements; iDx++)
{
// Some code
aList.nCount = someValue; //This line
}
aList.sort(function(a, b) { return b.nCount - a.nCount ; });
In the last release someone accidentally removed the line with comment.
and there was no other initialization of the member variable nCount.
Some of our clients got a "Number expected" exception which is pretty obvious (in retrospect), strangely the error doesn't reproduce with our Q.A. nor 80% of our clients!
How can it be? is there any strict mode that we can run with that will find such pesky bugs? what is the difference between the clients that got the exception and ones that didn't (it's not browser version nor windows version)
(our system runs only on IE6+ in a special container which makes it hard for us to write the code in normal I.D.E.'s we pretty much write it all in notepad ++)
You wrote int instead of var. I do that all the time...
int iDx=0 should be var iDx=0.
By the way, what editor are you using? int is a "future reserved word" in ES, so a good editor may highlight it in an ugly way (gedit makes it red with a red underline by default) to bring your attention to it.
Looks like it's a bug with IE6 related to array sorting.
Try some of the workarounds suggested here...

Javascript error in IE8: Not implemented

This one really puzzles me, as the code looks completely harmless.
IE8 halts script execution with a message:
Not implemented. map.js line:66 char:5
Here is a snip from the code:
63 if(data.map[x] !== undefined && data.map[x][y] !== undefined) {
64
65 left = (x - data.dim.x_min)*32 + 30;
66 top = (data.dim.y_max - y)*32 + 30;
67
68 /* do stuff */
XX }
debug info: x:263 data.dim.x_min:263 y:172 data.dim.y_max:174
Data is object returned from JQuery Ajax call. This works in Firefox 3.0 and 3.5, safari 4.0.2 and I've only found this error when viewing the page in IE8. Forcing IE8 into IE7 mode does not make the error go away.
I don't have IE7 to debug with, but I got a tester saying that it doesn't work in IE7 either.
The variable 'top' used in the code is an object of type DispHTMLWindow2 (outermost window object) and already in use by the browsers and that is causing the conflict, as that object cant be the target of the assignment operation. It seems that Firefox and Safari ignore this, while IE does not allow scripts to overwrite this.
Solutions for this:
1) Declare top you are using as local variable to define it's scope where it is used.
2) Rename the variable to something that doesn't conflict with this predefined global.
Description of other variable names you shouldn't use
IE 8 has a great javascript debugger. You might want to add a breakpoint somewhere before the error and step through the code to see if something is strange with the data. IE8 is picky with trailing commas in lists which might be why you only get the error in it. You can pull the debugger up with F12, click Script and choose start debugging. You can add a break point by clicking on the margin where the line numbers are.

Categories

Resources