Links

pmuellr is Patrick Mueller

other pmuellr thangs: home page, twitter, flickr, github

Wednesday, December 02, 2009

who called that function?

A question came up today in Web Inspector land - someone wanted to know why setTimeout() was being called so often in some web application. The suggestion was that a capability could be added to Web Inspector's timeline panel to show a function's invocations (setTimeout() in this case) as well as the stack traces for those calls, to figure out who was actually calling the function.

My immediate thought was - can't you do this with user-land code? If you can do it with plain old JavaScript, then I see less of a need to add the function to the debugger. Especially since:

  • The timeline already collects a LOT of information, keeping track of stack traces just adds more. The UI will get even busier, and Web Inspector will eat even more memory.

  • Some new gestures would be required to manage the list of functions that you wanted to see on the timeline, and whether you wanted the stack traces or not (if optional). Note that it's not possible to use the existing breakpoint gestures for this, as you can't set a breakpoint on built-in function (like setTimeout()).

I wasn't sure you could do this with user-land code, so I felt the need to figure out if you could or not. I got close, maybe close enough, and the result is up at GitHub as a gist with a wrapWithBackTrace() function. The idea is that you pass the function you want traced and back traces displayed for, into the wrapWithBackTrace() function, which returns a new function to use in place of the original. Invoking that function will dump information on the console, including a back trace of functions that called this function. I think there are enough comments in the code to figure out what's going on, but here are a few caveats:

  • The existing code makes use of Array's new methods indexOf(), map(), and forEach(). How nice to have these! If you don't have them, there's a link in the code to a site to get portable versions of them. Note that all the browsers I tested with (listed below) seemed to have them already. Wonderful!

  • The whole functionality revolves around the bizarre caller property of Function instances. Look it up in your favorite JavaScript reference. Supposedly this property will disappear in future implementations of JavaScript engines, and is already not available in some (IE, I think).

Some sample code that uses the wrapWithBackTrace() function is available in the same gist. Here's a bit of it:

function legend(message) {
    var dashes = new Array(60).join("-")
    console.log(dashes)
    console.log(message)
    console.log(dashes)
    console.log("")
}

// tests with user-land function

function factorial(n) {
    if (n <= 0) return 1
    return n * factorial(n-1)
}

function printFactorial(n) {
    console.log(n + "! == " + factorial(n))
    console.log("")
}

function a() { b() }
function b() { c() }
function c() { 
    printFactorial(0)
    printFactorial(5)
}

legend("calling factorial() before wrapping")
a()

// install the replacement function
factorial = wrapWithBackTrace(factorial)

legend("calling factorial() after wrapping")
a()

legend("calling factorial() at top level")
factorial(0)    

This code exercises some of the edge cases for the function - recursive functions, and functions run at the top level of a script (no back trace available). The output of running the code above, in the console, is:

-----------------------------------------------------------
calling factorial() before wrapping
-----------------------------------------------------------

0! == 1

5! == 120

-----------------------------------------------------------
calling factorial() after wrapping
-----------------------------------------------------------

backtrace for factorial()
   - printFactorial()
   - c()
   - b()
   - a()

0! == 1

backtrace for factorial()
   - factorial()
   - wrappedWithBackTrace()
   - ... recursion on factorial()

(... repeated invocations elided ...)

backtrace for factorial()
   - factorial()
   - wrappedWithBackTrace()
   - ... recursion on factorial()

backtrace for factorial()
   - printFactorial()
   - c()
   - b()
   - a()

5! == 120

-----------------------------------------------------------
calling factorial() at top level
-----------------------------------------------------------

backtrace for factorial()
   - {top level call}

Sample code is also provided that wraps setTimeout(), which worked the same way. Just wanted to make sure it worked with built-in functions as well as user-land functions.

The code works in the latest WebKit nightly, Safari 4.0.4, Firefox 3.5.5, Chrome 4.0.249.12, and Opera 10.10, all on Mac OS X 10.6. Note that Opera does not have console.log(), so I gave it one:

console = {log: opera.postError}

The code also works node.js. The node system also doesn't have a console.log() function, so I gave it one:

console = {log: require('sys').puts}

Actually, there's one tiny little WIBNI with node that shows up in the output - can you spot it? (you'll need to install node and try it yourself)

In the end, I kinda doubt the usefulness of this - the problem being that so much of JavaScript these days is anonymous functions. Without being able to get the "file name" and line number a function is defined on, and without a name for the function itself, having your backtrace be a list of {anonymous} is less that helpful. Seems like it would be handy to have a getStackTrace() method on Arguments to give you this information. This still wouldn't help with eval()'d code though, as it doesn't even have a "file name" associated with it.

No comments: