I'm starting to work on a dynamic analysis tool for JS and I'd like to profile an entire environment unobtrusively. I'm basically traversing various contexts, digging deep into objects, and every time I hit a function, I hook into it. Now, this works relatively well except for the fact that it breaks when dealing with libraries like jQuery/prototype etc.
This is my code thus far (commented to the best of my ability):
var __PROFILER_global_props = new Array(); // visited properties
/**
* Hook into a function
* @name the name of the function
* @fn the reference to the function
* @parent the parent object
*/
function __PROFILER_hook(name, fn, parent) {
//console.log('hooking ' + name + ' ' + fn + ' ' + parent);
if (typeof parent == 'undefined')
parent = window;
for (var i in parent) {
// find the right function
if (parent[i] === fn) {
// hook into it
console.log('--> hooking ' + name);
parent[i] = function() {
console.log('called ' + name);
return fn.apply(parent, arguments);
}
//parent[i] = fn; // <-- this works (obviously)
break;
}
}
}
/**
* Traverse object recursively, looking for functions or objects
* @obj the object we're going into
* @parent the parent (used for keeping a breadcrumb)
*/
function __PROFILER_traverse(obj, parent) {
for (i in obj) {
// get the toString object type
var oo = Object.prototype.toString.call(obj[i]);
// if we're NOT an object Object or an object Function
if (oo != '[object Object]' && oo != '[object Function]') {
console.log("...skipping " + i);
// skip
// ... the reason we do this is because Functions can have sub-functions and sub-objects (just like Objects)
continue;
}
if (__PROFILER_global_props.indexOf(i) == -1 // first we want to make sure we haven't already visited this property
&& (i != '__PROFILER_global_props' // we want to make sure we're not descending infinitely
&& i != '__PROFILER_traverse' // or recusrively hooking into our own hooking functions
&& i != '__PROFILER_hook' // ...
&& i != 'Event' // Event tends to be called a lot, so just skip it
&& i != 'log' // using FireBug for debugging, so again, avoid hooking into the logging functions
&& i != 'notifyFirebug')) { // another firebug quirk, skip this as well
// log the element we're looking at
console.log(parent+'.'+i);
// push it.. it's going to end up looking like '__PROFILER_BASE_.something.somethingElse.foo'
__PROFILER_global_props.push(parent+'.'+i);
try {
// traverse the property recursively
__PROFILER_traverse(obj[i], parent+'.'+i);
// hook into it (this function does nothing if obj[i] is not a function)
__PROFILER_hook(i, obj[i], obj);
} catch (err) {
// most likely a security exception. we don't care about this.
}
} else {
// some debugging
console.log(i + ' already visited');
}
}
}
That's the profile and this is how I invoke it:
// traverse the window
__PROFILER_traverse(window, '__PROFILER_BASE_');
// testing this on jQuery.com
$("p.neat").addClass("ohmy").show("slow");
Traversing works fine and hooking works fine as long as the functions are simple and non-anonymous (I think hooking into anonymous functions is impossible so I'm not too worried about it).
This is some trimmed output from the preprocessing phase.
notifyFirebug already visited
...skipping firebug
...skipping userObjects
__PROFILER_BASE_.loadFirebugConsole
--> hooking loadFirebugConsole
...skipping location
__PROFILER_BASE_.$
__PROFILER_BASE_.$.fn
__PROFILER_BASE_.$.fn.init
--> hooking init
...skipping selector
...skipping jquery
...skipping length
__PROFILER_BASE_.$.fn.size
--> hooking size
__PROFILER_BASE_.$.fn.toArray
--> hooking toArray
__PROFILER_BASE_.$.fn.get
--> hooking get
__PROFILER_BASE_.$.fn.pushStack
--> hooking pushStack
__PROFILER_BASE_.$.fn.each
--> hooking each
__PROFILER_BASE_.$.fn.ready
--> hooking ready
__PROFILER_BASE_.$.fn.eq
--> hooking eq
__PROFILER_BASE_.$.fn.first
--> hooking first
__PROFILER_BASE_.$.fn.last
--> hooking last
__PROFILER_BASE_.$.fn.slice
--> hooking slice
__PROFILER_BASE_.$.fn.map
--> hooking map
__PROFILER_BASE_.$.fn.end
--> hooking end
__PROFILER_BASE_.$.fn.push
--> hooking push
__PROFILER_BASE_.$.fn.sort
--> hooking sort
__PROFILER_BASE_.$.fn.splice
--> hooking splice
__PROFILER_BASE_.$.fn.extend
--> hooking extend
__PROFILER_BASE_.$.fn.data
--> hooking data
__PROFILER_BASE_.$.fn.removeData
--> hooking removeData
__PROFILER_BASE_.$.fn.queue
When I execute $("p.neat").addClass("ohmy").show("slow");
on jQuery.com (through Firebug), I get an appropriate call stack, but I seem to lose my context somewhere along the way because nothing happens and I get an e is undefined
error from jQuery (clearly, the hooking screwed something up).
called init
called init
called find
called find
called pushStack
called pushStack
called init
called init
called isArray
called isArray
called merge
called merge
called addClass
called addClass
called isFunction
called isFunction
called show
called show
called each
called each
called isFunction
called isFunction
called animate
called animate
called speed
called speed
called isFunction
called isFunction
called isEmptyObject
called isEmptyObject
called queue
called queue
called each
called each
called each
called each
called isFunction
called isFunction
The problem is that I think I'm losing the this
context when calling
return fn.apply(parent, arguments);
Here's another interesting quirk. If I hook before I traverse, i.e.:
// hook into it (this function does nothing if obj[i] is not a function)
__PROFILER_hook(i, obj[i], obj);
// traverse the property recursively
__PROFILER_traverse(obj[i], parent+'.'+i);
.. the application runs absolutely fine, but the call stack is altered (and I don't seem to get jQuery-specific functions) for some reason:
called $
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called setInterval
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called clearInterval
.. instead of animation
, show
, merge
, etc. Right now, all the hook does is say called functionName
but eventually I'd like to do stack traces and time functions (via a Java applet).
This question ended up being huge and I apologize, but any help is appreciated!
Note: the above code may crash your browser if you're not careful. Fair warning :P
I think you're on the right track. The value of this
is getting clobbered when you're using apply
. The functions defined within jQuery are probably being called via apply
internally, and depend on the value of this
.
The first argument to apply
is the value that will be used for this
. Are you sure that you should be using parent
for that?
I was able to duplicate the problem in the following manner:
var obj = {
fn : function() {
if(this == "monkeys") {
console.log("Monkeys are funny!");
}
else {
console.log("There are no monkeys :(");
}
}
};
obj.fn.apply("monkeys");
var ref = obj.fn;
//assuming parent here is obj
obj.fn = function() {
console.log("hooking to obj.fn");
return ref.apply(obj);
};
obj.fn.apply("monkeys");
Here, the function depends on the value of this
to print the text Monkeys are funny!
. As you can see, using your hook
algorithm, this context is lost. Firebug shows:
Monkeys are funny! hooking to obj.fn There are no monkeys :(
I made a slight change and used this
in the apply instead of obj
(the parent):
obj.fn = function() {
console.log("hooking to obj.fn");
return ref.apply(this);
};
This time Firebug says:
Monkeys are funny! hooking to obj.fn Monkeys are funny!
The root of your problem IMHO is that you're setting an explicit value for this
(i.e., parent
which refers to the parent object). So your hook function ends up overwriting the value of this
, which might have been explicitly set by whichever code that is calling the original function. Of course, that code doesn't know that you wrapped the original function with your own hook function. So your hook function should preserve the value of this
when it is calling the original function:
return fn.apply(this, arguments);
So if you use this
instead of parent
in your apply, your problem might be fixed.
I apologize if I haven't understood your problem correctly. Please correct me wherever I'm wrong.
UPDATE
There are two kinds of functions in jQuery. Ones that are attached to the jQuery
object itself (sort of like static methods) and then you have ones that operate on the result of jQuery(selector)
(sort of like instance methods). It's the latter you need to be concerned about. Here, this
matters a lot because that's how you implement chaining.
I was able to get the following example to work. Note that I am working on the instance of the object and not the object itself. So in your example, I would be working on jQuery("#someId")
and not on just jQuery
:
var obj = function(element) {
this.element = element;
this.fn0 = function(arg) {
console.log(arg, element);
return this;
}
this.fn1 = function(arg) {
console.log(arg, arg, element);
return this;
}
if(this instanceof obj) {
return this.obj;
}
else {
return new obj(element);
}
};
var objInst = obj("monkeys");
var ref0 = objInst.fn0;
objInst.fn0 = function(arg) {
console.log("calling f0");
return ref0.apply(this, [arg]);
};
var ref1 = objInst.fn1;
objInst.fn1 = function(arg) {
console.log("calling f1");
return ref1.apply(this, [arg]);
};
objInst.fn0("hello").fn1("bye");
I don't know if this solves your problem or not. Perhaps looking into the jQuery source will give you some more insight :). I think that the difficulty for you would be to distinguish between functions that are called via apply
and functions that are called via chaining (or are just directly called).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With