Say I want to get some kind of stack trace, getting the name of all the functions that have been called before the current function.
I do something like this:
var callee;
var caller;
var _args = arguments;
var check = 0;
do {
check++;
callee = _args.callee;
caller = callee.caller;
var msg = 'Check ' + check + ' - ' + callee.name
+ ' has been called by: ' + caller.name;
console.log(msg);
if (caller) {
// Get this caller's arguments
_args = caller.arguments;
} else {
reached_end = true;
}
} while (!reached_end);
This works fine, most of the time. But sometimes it gets stuck in an infinite loop, and I wonder: how is that possible? And what can I do about it?
Here's the output of my infinite-loop:
Check 1 - __parent__ has been called by: add
Check 2 - add has been called by: afterComponentStartup
Check 3 - afterComponentStartup has been called by: _launchComponents [arg0:"startup"]
Check 4 - _launchComponents has been called by: beforeActionNext
Check 5 - beforeActionNext has been called by: beforeAction
Check 6 - beforeAction has been called by: afterComponentInitialize
Check 7 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
Check 8 - _launchComponents has been called by: beforeActionNext
Check 9 - beforeActionNext has been called by: beforeAction
Check 10 - beforeAction has been called by: afterComponentInitialize
Check 11 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
Check 12 - _launchComponents has been called by: beforeActionNext
Check 13 - beforeActionNext has been called by: beforeAction
Check 14 - beforeAction has been called by: afterComponentInitialize
Check 15 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
Check 16 - _launchComponents has been called by: beforeActionNext
Check 17 - beforeActionNext has been called by: beforeAction
Check 18 - beforeAction has been called by: afterComponentInitialize
Check 19 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
Check 20 - _launchComponents has been called by: beforeActionNext
Check 21 - beforeActionNext has been called by: beforeAction
Check 22 - beforeAction has been called by: afterComponentInitialize
Check 23 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
Check 24 - _launchComponents has been called by: beforeActionNext
Check 25 - beforeActionNext has been called by: beforeAction
Check 26 - beforeAction has been called by: afterComponentInitialize
arguments.callee.caller points to the function reference, of which it only exists one of per function in each call stack.
Every time a function gets invoked the caller property gets set, which means that if the same function has been called multiple times in a call stack, as would happen with recursive functions, the previous value would get reset, and caller would now point to itself. This is what is causing your infinite loops.
So in your algorithm, if you get to a point where callee === callee.caller you need to break in order for this not to happen.
You need to add the callers in an array and check whether the next caller is in the array. If so then you have a cyclic or recursive call structure. Try this:
var args = arguments;
var callee = args.callee;
var caller = callee.caller;
var stack = [callee];
while (caller) {
if (stack.indexOf(caller) < 0) {
stack.push(caller);
args = caller.arguments;
callee = args.callee;
caller = callee.caller;
} else break;
}
console.log(stack);
I've found a way to pass state information (var .ua (Object) in this case) into the callstack of recursive functions;
instead of
var fwa.animatedJavascriptControlCenter = {
...
,
scanElements : function (el, scanResult) {
// a function that walks the DOM tree to find elements that require molding by my js framework
if (!scanResult) scanResult={};
if (el.tagName.toLowerCase()!='svg') {
if (conditions_for_this_element_met(el)) {
scanResults[el.id] = el;
};
if (el.children.length>0) {
for (var i=0; i < el.children.length; i++) {
if (el.children[i].tagName.toUpperCase()!=='IFRAME') {
scanResult = fwa.animatedJavascriptControlCenter.scanElements(el.children[i], scanResult);
}
}
}
}
return scanResult;
},
...
}
do:
var fwa.animatedJavascriptControlCenter = {
...
,
scanElements : function (el, scanResult) {
if (!scanResult) scanResult={};
if (el.tagName.toLowerCase()!='svg') {
if (conditions_for_this_element_met(el)) {
scanResults[el.id] = el;
}
if (el.children.length>0) {
for (var i=0; i < el.children.length; i++) {
if (el.children[i].tagName.toUpperCase()!=='IFRAME') {
var args = [el.children[i], scanResult];
args.ua= tracer.findUA(arguments);
var passUAfunc = function(scanResult) {
return fwa.animatedJavascriptControlCenter.scanElements(el.children[i], scanResult);
}
passUAfunc.ua = args.ua;
scanResult = passUAfunc(scanResult);
}
}
}
}
return scanResult;
},
...
}
to make this work, you'll also need:
// Bonus : ;-)
// Enable the passage of the 'this' object through the JavaScript timers
// thanks to https://developer.mozilla.org/en/docs/DOM/window.setTimeout#Callback_arguments
var __nativeST__ = window.setTimeout, __nativeSI__ = window.setInterval;
window.setTimeout = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */) {
var
oThis = this,
ua = tracer.findUA(arguments),
aArgs = Array.prototype.slice.call(arguments, 2);
if (ua) aArgs.ua = ua;
//if (!ua) debugger;
return __nativeST__(vCallback instanceof Function ? function () {
vCallback.apply(oThis, aArgs);
} : vCallback, nDelay);
};
window.setInterval = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */) {
var
oThis = this,
aArgs = Array.prototype.slice.call(arguments, 2),
ua = tracer.findUA(arguments);
if (ua) aArgs.ua = ua;
return __nativeSI__(vCallback instanceof Function ? function () {
vCallback.apply(oThis, aArgs);
} : vCallback, nDelay);
};
var tracer = {
/* object last modified : 14 May 2013, 04:46 CEST
original code by http://javascriptweblog.wordpress.com/2010/06/01/a-tracer-utility-in-2kb/
modified by rene7705@gmail.com with:
(1) http://stackoverflow.com/a/15582432/2379929
augmented by rene7705@gmail with (among other things):
http://stacktracejs.com/
this code is now used in and offered as part of the web-framework at http://fancywebapps.com
(component name : hipLog, to be released later)
*/
nativeCodeEx: /\[native code\]/,
tracing: [],
traced : [],
userActions : [],
findUA : function (arg) {
var p = arg;
if (p.ua) return p.ua;
var callee = arg.callee;
var caller = callee.caller;
var stack = [callee];
while (caller) {
if (stack.indexOf(caller) < 0) {
stack.push(caller);
args = caller.arguments;
callee = args.callee;
caller = callee.caller;
} else break;
}
while (p = stack.shift()) {
if (p.ua) return p.ua;
if (p.arguments && p.arguments.ua) return p.arguments.ua;
};
return false;
},
traceMe: function(func, methodName, path) {
var traceOn = function() {
var
ua = tracer.findUA(arguments),
startTime = +new Date;
if (!ua){
//debugger;
//ua = tracer.findUA(arguments);
};
//if (path=='fwa.animatedJavascriptControlCenter.scanElements') debugger;
if (!ua) {
//debugger;
var
uaIdx = tracer.userActions.length,
ua = tracer.userActions[uaIdx] = {
uaIdx : uaIdx,
startTime : startTime,
path : path,
stackLevel : 0
};
tracer.traced[uaIdx] = [];
} else {
var uaIdx = ua.uaIdx;
ua.stackLevel++;
}
arguments.ua = ua;
var idx = tracer.traced[uaIdx].length;
tracer.traced[uaIdx][idx] = {
path : path,
arguments : arguments
};
var result = func.apply(this, arguments);
tracer.traced[uaIdx][idx].stacktrace = printStackTrace() // see http://stacktracejs.com, free, quite useful
tracer.traced[uaIdx][idx].result = result;
tracer.traced[uaIdx][idx].timed = new Date - startTime;
tracer.traced[uaIdx][idx].stackLevel = ua.stackLevel;
ua.stackLevel--;
return result;
};
traceOn.traceOff = func;
for (var prop in func) {
traceOn[prop] = func[prop];
}
console.log("tracing " + path);
return traceOn;
},
traceAll: function(root, path, recurse) {
if ((root == window) || !((typeof root == 'object') || (typeof root == 'function'))) {return;}
for (var key in root) {
if ((root.hasOwnProperty(key)) && (root[key] != root)) {
var thisObj = root[key];
if (typeof thisObj == 'function') {
if ((this != root) && !thisObj.traceOff && !this.nativeCodeEx.test(thisObj)) {
root[key] = this.traceMe(root[key], key, path+'.'+key);
this.tracing.push({obj:root,methodName:key, path:path+'.'+key});
}
}
recurse && this.traceAll(thisObj, path+'.'+key, true);
}
}
},
untraceAll: function() {
for (var i=0; i<this.tracing.length; ++i) {
var thisTracing = this.tracing[i];
thisTracing.obj[thisTracing.methodName] =
thisTracing.obj[thisTracing.methodName].traceOff;
}
//console.log("tracing disabled");
tracer.tracing = [];
}
}
to be used like:
// call this just after _completely_ defining yourFrameworkRootObject and yourSiteCodeRootObject
tracer.traceAll (yourFrameworkRootObject, true);
tracer.traceAll (yourSiteCodeRootObject, true);
FYI: this code was taken from my js framework http://fancywebapps.com, and will be part of it's hipLog component that is designed to show you the complete execution path (with or without round trips to the server and/or setTimeout()s) of any userAction like clicks, hovers, or page loads.