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. each
2 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.
-
This word elides about two hours of moving window.alert calls around. ↩
-
A usual callback-first iterator. ↩