Material

Issues with Safari JIT arguments Handling

Debugging, step 1: It's always your bug, never the library, compiler, or OS.

Debugging, step ℵ: When it is the library / OS / compiler, god help you.

I spent hours of yesterday debugging what I can only conclude is a JIT bug in Safari (iOS 8 and OS X Safari 8 10600.1.22, though there's a good chance it's been in earlier versions given some issues I saw but ignored before). In the off chance someone else searching for "Safari JavaScript function not called" ever reads through results page 58 to reach this post, here we go:

The immediate symptoms were that the Yuu demo stopped moving (but kept tracking the mouse and rendering) after 10-12 clicks, and Pixel Witch Lesson #6 did... well, a lot of weird stuff in a similar vein, where it looked like things were only being half-processed or half-rendered. The common problem could only be described as "after a while, some functions turn into no-ops."

This is a bizarre symptom, especially when the code works fine in two other JavaScript engines. But what made me suspect it was a JIT bug rather than some ordinary bullshit cross-browser inconsistency is that I couldn't reproduce with debugging tools open - again, on OS X and remotely on iOS. As soon as the debugging window was open, the function was called 100% of the time. Debugging tools usually de-JIT or at least less-JIT code.

Unfortunately this means I couldn't debug it with a debugger, or even console.log debugging. I had to use window.alert.

Eventually1 I tracked the smaller Yuu test case down to a single line, inside my Animation class:

yf.each.call(this, this._dispatch, this.timeline[key]);

_dispatch was never being called, even though the array was non-empty. each2 is not a long function, but for reasons I'll probably write about one day, is both decorated and runs a function constructed with new Function(), which is to say it does Uncommon Things.

Replacing each with an equivalent for-loop fixed the problem. Wrapping the call to each in a try/catch block, another thing that often disables JIT optimization, also fixed it. But critically, wrapping the each function body did not - this strongly implicated the decorator, not anything each itself does.

The fault was in the decorator, a version of variadic written in a way to avoid constructing objects:

return function () {
    arguments[length - 1] = slice(arguments, length - 1);
    return f.apply(this, arguments);
};

By putting the copy-slice of arguments directly back into arguments, only one simple Array is constructed.

As far as I can tell this is perfectly legal, even in strict mode, but it sends Safari's optimizer off the deep end and functions get incorrect arguments. Because there's a lot of other functioning code where I use an arguments index as an l-value, I suspect it has to do with using it as both an l-value and an r-value in the same expression, then perhaps clobbering the wrong frame after some inlining. I can't find how to make Safari's JIT dump its generated code.

I've also been unable to reproduce it building "problem-up." It's something specific to the call pattern used by the Animation code and other things above it.

Well, so much for that plan. The new code has to make two Arrays:

return function () {
    var args = slice(arguments, 0, length - 1);
    args.push(slice(arguments, length - 1));
    return f.apply(this, args);
};

However, it does work.


  1. This word elides about two hours of moving window.alert calls around. 

  2. A usual callback-first iterator